First Upload

This commit is contained in:
2025-01-25 16:46:21 +00:00
commit 05358d3928
443 changed files with 181332 additions and 0 deletions

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM python:3.10.3-alpine
WORKDIR /rustdesk-api-server
ADD . /rustdesk-api-server
# 安装系统依赖
RUN apk add --no-cache \
gcc \
musl-dev \
mariadb-connector-c-dev \
pkgconfig
RUN set -ex \
&& pip install --no-cache-dir --disable-pip-version-check -r requirements.txt \
&& rm -rf /var/cache/apk/* \
&& cp -r ./db ./db_bak
ENV HOST="0.0.0.0"
ENV TZ="Asia/Shanghai"
EXPOSE 21114/tcp
EXPOSE 21114/udp
ENTRYPOINT ["sh", "run.sh"]

147
README.md Normal file
View File

@@ -0,0 +1,147 @@
# rustdesk-api-server
## based on https://github.com/kingmo888/rustdesk-api-server
![Main Page](images/user_devices.png)
![Connection Log](images/connection_log.png)
![File Transfer Log](images/file_log.png)
![Client Downloads](images/clients.png)
## Features
- Supports self-registration and login on the front-end webpage.
- Supports displaying device information on the front end, divided into administrator and user versions.
- Supports custom aliases (remarks).
- Supports backend management.
- Supports colored tags.
- Supports device online statistics.
- Supports saving device passwords.
- Automatically manages tokens and keeps them alive using the heartbeat interface.
- Supports sharing devices with other users.
- Supports web control terminal (currently only supports non-SSL mode, see below for usage issues)
## Installation
```bash
# open to the directory you want to install the api server (change /opt to wherever you want)
cd /opt
# Clone the code locally
git clone https://github.com/bryangerlach/rustdesk-api-server.git
# Enter the directory
cd rustdesk-api-server
# setup a python virtual environment called rdgen
python -m venv rustdesk-api
# activate the python virtual environment
source rustdesk-api/bin/activate
# Install dependencies
pip install -r requirements.txt
python manage.py migrate
# After ensuring dependencies are installed correctly, execute:
python manage.py runserver 0.0.0.0:21114
```
## ## To autostart the server on boot, you can set up a systemd service called rustdeskapi.service
```
[Unit]
Description=Rustdesk API Server
[Service]
Type=simple
LimitNOFILE=1000000
ExecStart=/opt/rustdesk-api-server/rustdesk-api/bin/python3 /opt/rustdesk-api-server/manage.py runserver 0.0.0.0:21114
WorkingDirectory=/opt/rustdesk-api-server/
User=root
Group=root
Restart=always
StandardOutput=file:/var/log/rustdesk/apiserver.log
StandardError=file:/var/log/rustdesk/apiserver.error
# Restart service after 10 seconds if node service crashes
RestartSec=10
[Install]
WantedBy=multi-user.target
```
## Updating
```bash
#this assumes you have cloned the repo into /opt/rustdesk/rustdesk-api-server and have a service named rustdeskapi set up
systemctl stop rustdeskapi
cd /opt/rustdesk/rustdesk-api-server
git pull
source rustdesk-api/bin/activate
pip install -r requirements.txt
python manage.py migrate
systemctl start rustdeskapi
```
**Client Downlaods**: You will need to generate your own client downloads with your server and key hard coded into the program. The easiest way to do this is using github actions https://rustdesk.com/docs/en/dev/build/all/
Now you can access it using `http://localhostIP:Port`.
**Note**: When configuring on CentOS, Django4 may have problems due to the low version of sqlite3 in the system. Please modify the file in the dependency library. Path: `xxxx/Lib/site-packages/django/db/backends/sqlite3/base.py` (Find the package address according to the situation), modify the content:
```python
# from sqlite3 import dbapi2 as Database #(comment out this line)
from pysqlite3 import dbapi2 as Database # enable pysqlite3
```
## Environment Variables
| Variable Name | Reference Value | Note |
| ---- | ------- | ----------- |
| `HOST` | Default `0.0.0.0` | IP binding of the service |
| `TZ` | Default `Asia/Shanghai`, optional | Timezone |
| `SECRET_KEY` | Optional, custom a random string | Program encryption key |
| `CSRF_TRUSTED_ORIGINS` | Optional, verification off by default;<br>If you need to enable it, fill in your access address `http://yourdomain.com:21114` <br>**To disable verification, please delete this variable instead of leaving it blank** | Cross-origin trusted source |
| `ID_SERVER` | Optional, default is the same host as the API server.<br>Customizable like `yourdomain.com` | ID server used by the web control terminal |
| `DEBUG` | Optional, default `False` | Debug mode |
| `ALLOW_REGISTRATION` | Optional, default `True` | Whether to allow new user registration |
| Database Configuration | -- Start -- | If not using MYSQL, the following are unnecessary |
| `DATABASE_TYPE` | Optional, default `SQLITE3` | Database type (SQLITE/MYSQL) |
| `MYSQL_DBNAME` | Optional, default `-` | MYSQL database name |
| `MYSQL_HOST` | Optional, default `127.0.0.1` | MYSQL database server IP |
| `MYSQL_USER` | Optional, default `-` | MYSQL database username |
| `MYSQL_PASSWORD` | Optional, default `-` | MYSQL database password |
| `MYSQL_PORT` | Optional, default `3306` | MYSQL database port |
| Database Configuration | -- End -- | See [sqlite3 migration to mysql tutorial](/tutorial/sqlite2mysql.md) |
## Usage Issues
- Administrator Settings
When there are no accounts in the database, the first registered account directly obtains super administrator privileges,
and subsequently registered accounts are ordinary accounts.
- Device Information
Tested, the client will send device information to the API interface regularly in the mode of installation as a service under non-green mode, so if you want device information, you need to install the Rustdesk client and start the service.
- Slow Connection Speed
Use the client generator to generate a client that has the connection delay removed
- Web Control Terminal Configuration
- Set the ID_SERVER environment variable or modify the ID_SERVER configuration item in the rustdesk_server_api/settings.py file and fill in the IP or domain name of the ID server/relay server.
- Web Control Terminal Keeps Spinning
- Check if the ID server filling is correct.
- The web control terminal currently only supports non-SSL mode. If the webui is accessed via https, remove the 's', otherwise ws cannot connect and keeps spinning. For example: https://domain.com/webui, change to http://domain.com/webui
- CSRF verification failed when logging in or logging out of backend operations. Request interrupted.
This operation is highly likely to be a combination of docker configuration + nginx reverse proxy + SSL. Pay attention to modifying CSRF_TRUSTED_ORIGINS. If it is SSL, it starts with https, otherwise it is http.
## Other Related Tools
- [rdgen](https://github.com/bryangerlach/rdgen)
- [infinite remote](https://github.com/infiniteremote/installer)
- [CMD script for modifying client ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer)
- [rustdesk](https://github.com/rustdesk/rustdesk)
- [rustdesk-server](https://github.com/rustdesk/rustdesk-server)

210
README_EN.md Normal file
View File

@@ -0,0 +1,210 @@
# rustdesk-api-server
## If the project has helped you, giving a star isn't too much, right?
## Please use the latest version 1.2.3 of the client.
[点击这里查看中文说明。](https://github.com/kingmo888/rustdesk-api-server/blob/master/README.md)
<p align="center">
<i>A Rustdesk API interface implemented in Python, with WebUI management support</i>
<br/>
<img src ="https://img.shields.io/badge/Version-1.5.0-blueviolet.svg"/>
<img src ="https://img.shields.io/badge/Python-3.7|3.8|3.9|3.10|3.11-blue.svg" />
<img src ="https://img.shields.io/badge/Django-3.2+|4.x-yelow.svg" />
<br/>
<img src ="https://img.shields.io/badge/Platform-Windows|Linux-green.svg"/>
<img src ="https://img.shields.io/badge/Docker-arm|arm64|amd64-blue.svg" />
</p>
![Main Page](images/front_main.png)
## Features
- Supports self-registration and login on the front-end webpage.
- Registration and login pages:
![Front Registration](images/front_reg.png)
![Front Login](images/front_login.png)
- Supports displaying device information on the front end, divided into administrator and user versions.
- Supports custom aliases (remarks).
- Supports backend management.
- Supports colored tags.
![Rust Books](images/rust_books.png)
- Supports device online statistics.
- Supports saving device passwords.
- Automatically manages tokens and keeps them alive using the heartbeat interface.
- Supports sharing devices with other users.
![Rust Share](images/share.png)
- Supports web control terminal (currently only supports non-SSL mode, see below for usage issues)
![Rust Share](images/webui.png)
Admin Home Page:
![Admin Main](images/admin_main.png)
## Installation
### Method 1: Out-of-the-box
Only supports Windows, please go to the release to download, no need to install environment, just run `启动.bat` directly. Screenshots:
![Windows Run Directly Version](/images/windows_run.png)
### Method 2: Running the Code
```bash
# Clone the code locally
git clone https://github.com/kingmo888/rustdesk-api-server.git
# Enter the directory
cd rustdesk-api-server
# Install dependencies
pip install -r requirements.txt
# After ensuring dependencies are installed correctly, execute:
# Please modify the port number yourself, it is recommended to keep 21114 as the default port for Rustdesk API
python manage.py runserver 0.0.0.0:21114
```
Now you can access it using `http://localhostIP:Port`.
**Note**: When configuring on CentOS, Django4 may have problems due to the low version of sqlite3 in the system. Please modify the file in the dependency library. Path: `xxxx/Lib/site-packages/django/db/backends/sqlite3/base.py` (Find the package address according to the situation), modify the content:
```python
# from sqlite3 import dbapi2 as Database #(comment out this line)
from pysqlite3 import dbapi2 as Database # enable pysqlite3
```
### Method 3: Docker Run
#### Docker Method 1: Build Yourself
```bash
git clone https://github.com/kingmo888/rustdesk-api-server.git
cd rustdesk-api-server
docker compose --compatibility up --build -d
```
Thanks to the enthusiastic netizen @ferocknew for providing.
#### Docker Method 2: Pre-built Run
docker run command:
```bash
docker run -d \
--name rustdesk-api-server \
-p 21114:21114 \
-e CSRF_TRUSTED_ORIGINS=http://yourdomain.com:21114 \ #Cross-origin trusted source, optional
-e ID_SERVER=yourdomain.com \ #ID server used by the web control terminal
-v /yourpath/db:/rustdesk-api-server/db \ #Modify /yourpath/db to your host database mount directory
-v /etc/timezone:/etc/timezone:ro \
-v /etc/localtime:/etc/localtime:ro \
--network bridge \
--restart unless-stopped \
ghcr.io/kingmo888/rustdesk-api-server:latest
```
docker-compose method:
```yaml
version: "3.8"
services:
rustdesk-api-server:
container_name: rustdesk-api-server
image: ghcr.io/kingmo888/rustdesk-api-server:latest
environment:
- CSRF_TRUSTED_ORIGINS=http://yourdomain.com:21114 #Cross-origin trusted source, optional
- ID_SERVER=yourdomain.com #ID server used by the web control terminal
volumes:
- /yourpath/db:/rustdesk-api-server/db #Modify /yourpath/db to your host database mount directory
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
network_mode: bridge
ports:
- "21114:21114"
restart: unless-stopped
```
## Environment Variables
| Variable Name | Reference Value | Note |
| ---- | ------- | ----------- |
| `HOST` | Default `0.0.0.0` | IP binding of the service |
| `TZ` | Default `Asia/Shanghai`, optional | Timezone |
| `SECRET_KEY` | Optional, custom a random string | Program encryption key |
| `CSRF_TRUSTED_ORIGINS` | Optional, verification off by default;<br>If you need to enable it, fill in your access address `http://yourdomain.com:21114` <br>**To disable verification, please delete this variable instead of leaving it blank** | Cross-origin trusted source |
| `ID_SERVER` | Optional, default is the same host as the API server.<br>Customizable like `yourdomain.com` | ID server used by the web control terminal |
| `DEBUG` | Optional, default `False` | Debug mode |
| `ALLOW_REGISTRATION` | Optional, default `True` | Whether to allow new user registration |
| Database Configuration | -- Start -- | If not using MYSQL, the following are unnecessary |
| `DATABASE_TYPE` | Optional, default `SQLITE3` | Database type (SQLITE/MYSQL) |
| `MYSQL_DBNAME` | Optional, default `-` | MYSQL database name |
| `MYSQL_HOST` | Optional, default `127.0.0.1` | MYSQL database server IP |
| `MYSQL_USER` | Optional, default `-` | MYSQL database username |
| `MYSQL_PASSWORD` | Optional, default `-` | MYSQL database password |
| `MYSQL_PORT` | Optional, default `3306` | MYSQL database port |
| Database Configuration | -- End -- | See [sqlite3 migration to mysql tutorial](/tutorial/sqlite2mysql.md) |
## Usage Issues
- Administrator Settings
When there are no accounts in the database, the first registered account directly obtains super administrator privileges,
and subsequently registered accounts are ordinary accounts.
- Device Information
Tested, the client will send device information to the API interface regularly in the mode of installation as a service under non-green mode, so if you want device information, you need to install the Rustdesk client and start the service.
- Slow Connection Speed
The new version Key mode connection speed is slow. You can start the service on the server without the -k parameter. At this time, the client cannot configure the key either.
- Web Control Terminal Configuration
- Set the ID_SERVER environment variable or modify the ID_SERVER configuration item in the rustdesk_server_api/settings.py file and fill in the IP or domain name of the ID server/relay server.
- Web Control Terminal Keeps Spinning
- Check if the ID server filling is correct.
- The web control terminal currently only supports non-SSL mode. If the webui is accessed via https, remove the 's', otherwise ws cannot connect and keeps spinning. For example: https://domain.com/webui, change to http://domain.com/webui
- CSRF verification failed when logging in or logging out of backend operations. Request interrupted.
This operation is highly likely to be a combination of docker configuration + nginx reverse proxy + SSL. Pay attention to modifying CSRF_TRUSTED_ORIGINS. If it is SSL, it starts with https, otherwise it is http.
## Development Plans
- [x] Share devices with other registered users (v1.3+)
> Explanation: Similar to sharing URLs of network disks, the URL can be activated to obtain devices under a certain group or certain label.
> Note: In fact, there is not much that can be done with the web API as middleware. More functions still need to be implemented by modifying the client, which is not very worthwhile.
- [x] Integration of Web client form (v1.4+)
> Integrating the great god's web client, already integrated. [Source](https://www.52pojie.cn/thread-1708319-1-1.html)
- [x] Filter expired (offline) devices to distinguish between online and offline devices (1.4.7)
> By configuration, clean or filter devices that have expired for more than a specified time.
- [x] Split the first screen into user list page and administrator list page and add pagination (1.4.6).
- [x] Support exporting information to xlsx files (1.4.6).
> Allows administrators to export all device information on the [All Devices] page.
- [x] Set whether to allow new user registration through configuration items (1.4.7).
- [x] Support mysql and sqlite3 migration to mysql (1.4.8).
## Other Related Tools
- [CMD script for modifying client ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer)
- [rustdesk](https://github.com/rustdesk/rustdesk)
- [rustdesk-server](https://github.com/rustdesk/rustdesk-server)
## Stargazers over time
[![Stargazers over time](https://starchart.cc/kingmo888/rustdesk-api-server.svg?variant=adaptive)](https://starchart.cc/kingmo888/rustdesk-api-server)

0
api/__init__.py Normal file
View File

1
api/admin.py Normal file
View File

@@ -0,0 +1 @@
from .admin_user import *

101
api/admin_user.py Normal file
View File

@@ -0,0 +1,101 @@
# cython:language_level=3
from django.contrib import admin
from api import models
from django import forms
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.utils.translation import gettext as _
class UserCreationForm(forms.ModelForm):
"""A form for creating new users. Includes all the required
fields, plus a repeated password."""
password1 = forms.CharField(label=_('密码'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('再次输入密码'), widget=forms.PasswordInput)
class Meta:
model = models.UserProfile
fields = ('username','is_active','is_admin')
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("密码校验失败,两次密码不一致。"))
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super(UserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
"""A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
password hash display field.
"""
password = ReadOnlyPasswordHashField(label=(_("密码Hash值")), help_text=("<a href=\"../password/\">点击修改密码</a>."))
class Meta:
model = models.UserProfile
fields = ('username', 'is_active', 'is_admin')
def clean_password(self):
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial["password"]
#return self.initial["password"]
def save(self, commit=True):
# Save the provided password in hashed format
user = super(UserChangeForm, self).save(commit=False)
if commit:
user.save()
return user
class UserAdmin(BaseUserAdmin):
# The forms to add and change user instances
form = UserChangeForm
add_form = UserCreationForm
password = ReadOnlyPasswordHashField(label=("Password HASH value"), help_text=("<a href=\"../password/\">Click to modify the password</a>."))
# The fields to be used in displaying the User model.
# These override the definitions on the base UserAdmin
# that reference specific fields on auth.User.
list_display = ('username', 'rid')
list_filter = ('is_admin', 'is_active')
fieldsets = (
(_('基本信息'), {'fields': ('username', 'password', 'is_active', 'is_admin', 'rid', 'uuid', 'deviceInfo',)}),
)
readonly_fields = ( 'rid', 'uuid')
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'is_active', 'is_admin', 'password1', 'password2', )}
),
)
search_fields = ('username', )
ordering = ('username',)
filter_horizontal = ()
admin.site.register(models.UserProfile, UserAdmin)
admin.site.register(models.RustDeskToken, models.RustDeskTokenAdmin)
admin.site.register(models.RustDeskTag, models.RustDeskTagAdmin)
admin.site.register(models.RustDeskPeer, models.RustDeskPeerAdmin)
admin.site.register(models.RustDesDevice, models.RustDesDeviceAdmin)
admin.site.register(models.ShareLink, models.ShareLinkAdmin)
admin.site.register(models.ConnLog, models.ConnLogAdmin)
admin.site.register(models.FileLog, models.FileLogAdmin)
admin.site.unregister(Group)
admin.site.site_header = _('RustDesk自建Web')
admin.site.site_title = _('未定义')

5
api/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = 'api'

102
api/forms.py Normal file
View File

@@ -0,0 +1,102 @@
from django import forms
from api.models import UserProfile
class GenerateForm(forms.Form):
#Platform
platform = forms.ChoiceField(choices=[('windows','Windows'),('linux','Linux (currently unavailable)'),('android','Android (testing now available)')], initial='windows')
version = forms.ChoiceField(choices=[('master','beta'),('1.3.2','1.3.2'),('1.3.1','1.3.1'),('1.3.0','1.3.0')], initial='1.3.2')
delayFix = forms.BooleanField(initial=True, required=False)
#General
exename = forms.CharField(label="Name for EXE file", required=True)
appname = forms.CharField(label="Custom App Name", required=False)
direction = forms.ChoiceField(widget=forms.RadioSelect, choices=[
('incoming', 'Incoming Only'),
('outgoing', 'Outgoing Only'),
('both', 'Bidirectional')
], initial='both')
installation = forms.ChoiceField(label="Disable Installation", choices=[
('installationY', 'No, enable installation'),
('installationN', 'Yes, DISABLE installation')
], initial='installationY')
settings = forms.ChoiceField(label="Disable Settings", choices=[
('settingsY', 'No, enable settings'),
('settingsN', 'Yes, DISABLE settings')
], initial='settingsY')
#Custom Server
serverIP = forms.CharField(label="Host", required=False)
apiServer = forms.CharField(label="API Server", required=False)
key = forms.CharField(label="Key", required=False)
urlLink = forms.CharField(label="Custom URL for links", required=False)
#Visual
iconfile = forms.FileField(label="Custom App Icon (in .png format)", required=False, widget=forms.FileInput(attrs={'accept': 'image/png'}))
logofile = forms.FileField(label="Custom App Logo (in .png format)", required=False, widget=forms.FileInput(attrs={'accept': 'image/png'}))
theme = forms.ChoiceField(choices=[
('light', 'Light'),
('dark', 'Dark'),
('system', 'Follow System')
], initial='system')
themeDorO = forms.ChoiceField(choices=[('default', 'Default'),('override', 'Override')], initial='default')
#Security
passApproveMode = forms.ChoiceField(choices=[('password','Accept sessions via password'),('click','Accept sessions via click'),('password-click','Accepts sessions via both')],initial='password-click')
permanentPassword = forms.CharField(widget=forms.PasswordInput(), required=False)
runasadmin = forms.ChoiceField(choices=[('false','No'),('true','Yes')], initial='false')
denyLan = forms.BooleanField(initial=False, required=False)
enableDirectIP = forms.BooleanField(initial=False, required=False)
#ipWhitelist = forms.BooleanField(initial=False, required=False)
autoClose = forms.BooleanField(initial=False, required=False)
#Permissions
permissionsDorO = forms.ChoiceField(choices=[('default', 'Default'),('override', 'Override')], initial='default')
permissionsType = forms.ChoiceField(choices=[('custom', 'Custom'),('full', 'Full Access'),('view','Screen share')], initial='custom')
enableKeyboard = forms.BooleanField(initial=True, required=False)
enableClipboard = forms.BooleanField(initial=True, required=False)
enableFileTransfer = forms.BooleanField(initial=True, required=False)
enableAudio = forms.BooleanField(initial=True, required=False)
enableTCP = forms.BooleanField(initial=True, required=False)
enableRemoteRestart = forms.BooleanField(initial=True, required=False)
enableRecording = forms.BooleanField(initial=True, required=False)
enableBlockingInput = forms.BooleanField(initial=True, required=False)
enableRemoteModi = forms.BooleanField(initial=False, required=False)
#Other
removeWallpaper = forms.BooleanField(initial=True, required=False)
defaultManual = forms.CharField(widget=forms.Textarea, required=False)
overrideManual = forms.CharField(widget=forms.Textarea, required=False)
class AddPeerForm(forms.Form):
clientID = forms.CharField(label="Client Rustdesk ID", required=True)
alias = forms.CharField(label="Client alias", required=True)
tags = forms.CharField(label="Tags", required=False)
username = forms.CharField(label="Username", required=False)
hostname = forms.CharField(label="OS", required=False)
platform = forms.CharField(label="Platform", required=False)
ip = forms.CharField(label="IP", required=False)
class AssignPeerForm(forms.Form):
uid = forms.ModelChoiceField(
queryset=UserProfile.objects.all(),
to_field_name='id',
empty_label='Select a User',
required=True
)
clientID = forms.CharField(label="Client Rustdesk ID", required=True)
alias = forms.CharField(label="Client alias", required=True)
tags = forms.CharField(label="Tags", required=False)
username = forms.CharField(label="Username", required=False)
hostname = forms.CharField(label="Hostname", required=False)
platform = forms.CharField(label="Platform", required=False)
ip = forms.CharField(label="IP", required=False)
class EditPeerForm(forms.Form):
clientID = forms.CharField(label="Client Rustdesk ID", required=True)
alias = forms.CharField(label="Client alias", required=True)
tags = forms.CharField(label="Tags", required=False)
username = forms.CharField(label="Username", required=False)
hostname = forms.CharField(label="OS", required=False)
platform = forms.CharField(label="Platform", required=False)
ip = forms.CharField(label="IP", required=False)

78
api/front_locale.py Normal file
View File

@@ -0,0 +1,78 @@
from django.utils.translation import gettext as _
_('管理后台')
_('ID列表')
_('分享机器')
_('这么简易的东西,忘记密码这功能就没必要了吧。')
_('立即注册')
_('创建时间')
_('注册成功,请前往登录页登录。')
_('注册日期')
_('2、所分享的机器被分享人享有相同的权限如果机器设置了保存密码被分享人也可以直接连接。')
_('导出xlsx')
_('生成分享链接')
_('请输入8~20位密码。可以包含字母、数字和特殊字符。')
_('尾页')
_('请确认密码')
_('注册')
_('内存')
_('首页')
_('网页控制')
_('注册时间')
_('链接地址')
_('请输入密码')
_('系统用户名')
_('状态')
_('已有账号?立即登录')
_('密码')
_('别名')
_('上一页')
_('更新时间')
_('综合屏')
_('平台')
_('全部用户')
_('注册页')
_('分享机器给其他用户')
_('所有设备')
_('连接密码')
_('设备统计')
_('所属用户')
_('分享')
_('请输入用户名')
_('1、链接有效期为15分钟切勿随意分享给他人。')
_('CPU')
_('客户端ID')
_('下一页')
_('登录')
_('退出')
_('请将要分享的机器调整到右侧')
_('成功!如需分享,请复制以下链接给其他人:<br>')
_('忘记密码?')
_('计算机名')
_('两次输入密码不一致!')
_('页码')
_('版本')
_('用户名')
_('3、为保障安全链接有效期为15分钟、链接仅有效1次。链接一旦被非分享人的登录用户访问分享生效后续访问链接失效。')
_('系统')
_('我的机器')
_('信息')
_('远程ID')
_('远程别名')
_('用户ID')
_('用户别名')
_('用户IP')
_('文件大小')
_('发送/接受')
_('记录于')
_('连接开始时间')
_('连接结束时间')
_('时长')
_('连接日志')
_('文件传输日志')
_('页码 #')
_('下一页 #')
_('上一页 #')
_('第一页')
_('上页')

View File

@@ -0,0 +1,243 @@
# Generated by Django 4.2.7 on 2023-12-14 12:08
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name="RustDesDevice",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"rid",
models.CharField(blank=True, max_length=60, verbose_name="Client ID"),
),
("cpu", models.CharField(max_length=20, verbose_name="CPU")),
("hostname", models.CharField(max_length=20, verbose_name="CPU name")),
("memory", models.CharField(max_length=20, verbose_name="Memory")),
("os", models.CharField(max_length=20, verbose_name="operating system")),
("uuid", models.CharField(max_length=60, verbose_name="uuid")),
(
"username",
models.CharField(blank=True, max_length=60, verbose_name="System username"),
),
("version", models.CharField(max_length=20, verbose_name="Client version")),
(
"create_time",
models.DateTimeField(auto_now_add=True, verbose_name="Equipment registration time"),
),
(
"update_time",
models.DateTimeField(auto_now=True, verbose_name="Equipment update time"),
),
],
options={
"verbose_name": "equipment",
"verbose_name_plural": "Device List",
"ordering": ("-rid",),
},
),
migrations.CreateModel(
name="RustDeskPeer",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("uid", models.CharField(max_length=16, verbose_name="User ID")),
("rid", models.CharField(max_length=60, verbose_name="Client ID")),
("username", models.CharField(max_length=20, verbose_name="System username")),
("hostname", models.CharField(max_length=30, verbose_name="Operating system name")),
("alias", models.CharField(max_length=30, verbose_name="Alias")),
("platform", models.CharField(max_length=30, verbose_name="platform")),
("tags", models.CharField(max_length=30, verbose_name="Label")),
("rhash", models.CharField(max_length=60, verbose_name="Device link password")),
],
options={
"verbose_name": "Peers",
"verbose_name_plural": "PEERS list",
"ordering": ("-username",),
},
),
migrations.CreateModel(
name="RustDeskTag",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("uid", models.CharField(max_length=16, verbose_name="User ID")),
("tag_name", models.CharField(max_length=60, verbose_name="Tag name")),
(
"tag_color",
models.CharField(blank=True, max_length=60, verbose_name="Tag color"),
),
],
options={
"verbose_name": "Tags",
"verbose_name_plural": "Tags list",
"ordering": ("-uid",),
},
),
migrations.CreateModel(
name="RustDeskToken",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("username", models.CharField(max_length=20, verbose_name="username")),
("rid", models.CharField(max_length=16, verbose_name="RustDesk ID")),
("uid", models.CharField(max_length=16, verbose_name="User ID")),
("uuid", models.CharField(max_length=60, verbose_name="uuid")),
(
"access_token",
models.CharField(
blank=True, max_length=60, verbose_name="access_token"
),
),
(
"create_time",
models.DateTimeField(auto_now_add=True, verbose_name="Log in time"),
),
],
options={
"verbose_name": "Token",
"verbose_name_plural": "Token list",
"ordering": ("-username",),
},
),
migrations.CreateModel(
name="ShareLink",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("uid", models.CharField(max_length=16, verbose_name="User ID")),
("shash", models.CharField(max_length=60, verbose_name="Link Key")),
("peers", models.CharField(max_length=20, verbose_name="Machine ID list")),
("is_used", models.BooleanField(default=False, verbose_name="use or not")),
("is_expired", models.BooleanField(default=False, verbose_name="Whether to expire")),
(
"create_time",
models.DateTimeField(auto_now_add=True, verbose_name="Generation time"),
),
],
options={
"verbose_name": "Share link",
"verbose_name_plural": "Link list",
"ordering": ("-create_time",),
},
),
migrations.CreateModel(
name="UserProfile",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(max_length=50, unique=True, verbose_name="username"),
),
("rid", models.CharField(max_length=16, verbose_name="RustDesk ID")),
("uuid", models.CharField(max_length=60, verbose_name="uuid")),
(
"autoLogin",
models.BooleanField(default=True, verbose_name="autoLogin"),
),
("rtype", models.CharField(max_length=20, verbose_name="rtype")),
("deviceInfo", models.TextField(blank=True, verbose_name="login information:")),
("is_active", models.BooleanField(default=True, verbose_name="Activate now")),
("is_admin", models.BooleanField(default=False, verbose_name="Whether a administrator")),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
],
options={
"verbose_name": "user",
"verbose_name_plural": "user list",
"permissions": (
("view_task", "Can see available tasks"),
("change_task_status", "Can change the status of tasks"),
("close_task", "Can remove a task by setting its status as closed"),
),
},
),
]

View File

@@ -0,0 +1,48 @@
# Generated by Django 4.2.7 on 2024-02-21 10:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="rustdesdevice",
name="cpu",
field=models.CharField(max_length=100, verbose_name="CPU"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="hostname",
field=models.CharField(max_length=100, verbose_name="主机名"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="memory",
field=models.CharField(max_length=100, verbose_name="内存"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="os",
field=models.CharField(max_length=100, verbose_name="操作系统"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="username",
field=models.CharField(blank=True, max_length=100, verbose_name="系统用户名"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="uuid",
field=models.CharField(max_length=100, verbose_name="uuid"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="version",
field=models.CharField(max_length=100, verbose_name="客户端版本"),
),
]

View File

@@ -0,0 +1,238 @@
# Generated by Django 4.2.7 on 2024-03-15 20:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0002_alter_rustdesdevice_cpu_alter_rustdesdevice_hostname_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="rustdesdevice",
options={
"ordering": ("-rid",),
"verbose_name": "Device",
"verbose_name_plural": "Device List",
},
),
migrations.AlterModelOptions(
name="rustdeskpeer",
options={
"ordering": ("-username",),
"verbose_name": "Peers",
"verbose_name_plural": "Peers List",
},
),
migrations.AlterModelOptions(
name="rustdesktag",
options={
"ordering": ("-uid",),
"verbose_name": "Tags",
"verbose_name_plural": "Tags List",
},
),
migrations.AlterModelOptions(
name="rustdesktoken",
options={
"ordering": ("-username",),
"verbose_name": "Token",
"verbose_name_plural": "Token List",
},
),
migrations.AlterModelOptions(
name="sharelink",
options={
"ordering": ("-create_time",),
"verbose_name": "Share Link",
"verbose_name_plural": "Link List",
},
),
migrations.AlterModelOptions(
name="userprofile",
options={
"permissions": (
("view_task", "Can see available tasks"),
("change_task_status", "Can change the status of tasks"),
("close_task", "Can remove a task by setting its status as closed"),
),
"verbose_name": "User",
"verbose_name_plural": "User List",
},
),
migrations.AlterField(
model_name="rustdesdevice",
name="create_time",
field=models.DateTimeField(
auto_now_add=True, verbose_name="Device Registration Time"
),
),
migrations.AlterField(
model_name="rustdesdevice",
name="hostname",
field=models.CharField(max_length=100, verbose_name="Hostname"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="memory",
field=models.CharField(max_length=100, verbose_name="Memory"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="os",
field=models.CharField(max_length=100, verbose_name="Operating System"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="rid",
field=models.CharField(blank=True, max_length=60, verbose_name="Client ID"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="username",
field=models.CharField(
blank=True, max_length=100, verbose_name="System Username"
),
),
migrations.AlterField(
model_name="rustdesdevice",
name="version",
field=models.CharField(max_length=100, verbose_name="Client Version"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="alias",
field=models.CharField(max_length=30, verbose_name="Alias"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="hostname",
field=models.CharField(max_length=30, verbose_name="Operating System Name"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="platform",
field=models.CharField(max_length=30, verbose_name="Platform"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="rhash",
field=models.CharField(
max_length=60, verbose_name="Device Connection Password"
),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="rid",
field=models.CharField(max_length=60, verbose_name="Client ID"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="tags",
field=models.CharField(max_length=30, verbose_name="Tag"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="uid",
field=models.CharField(max_length=16, verbose_name="User ID"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="username",
field=models.CharField(max_length=20, verbose_name="System Username"),
),
migrations.AlterField(
model_name="rustdesktag",
name="tag_color",
field=models.CharField(blank=True, max_length=60, verbose_name="Tag Color"),
),
migrations.AlterField(
model_name="rustdesktag",
name="tag_name",
field=models.CharField(max_length=60, verbose_name="Tag Name"),
),
migrations.AlterField(
model_name="rustdesktag",
name="uid",
field=models.CharField(max_length=16, verbose_name="Belongs to User ID"),
),
migrations.AlterField(
model_name="rustdesktoken",
name="access_token",
field=models.CharField(
blank=True, max_length=60, verbose_name="Access Token"
),
),
migrations.AlterField(
model_name="rustdesktoken",
name="create_time",
field=models.DateTimeField(auto_now_add=True, verbose_name="Login Time"),
),
migrations.AlterField(
model_name="rustdesktoken",
name="uid",
field=models.CharField(max_length=16, verbose_name="User ID"),
),
migrations.AlterField(
model_name="rustdesktoken",
name="username",
field=models.CharField(max_length=20, verbose_name="Username"),
),
migrations.AlterField(
model_name="rustdesktoken",
name="uuid",
field=models.CharField(max_length=60, verbose_name="UUID"),
),
migrations.AlterField(
model_name="sharelink",
name="create_time",
field=models.DateTimeField(auto_now_add=True, verbose_name="Creation Time"),
),
migrations.AlterField(
model_name="sharelink",
name="is_expired",
field=models.BooleanField(default=False, verbose_name="Is Expired"),
),
migrations.AlterField(
model_name="sharelink",
name="is_used",
field=models.BooleanField(default=False, verbose_name="Is Used"),
),
migrations.AlterField(
model_name="sharelink",
name="peers",
field=models.CharField(max_length=20, verbose_name="Machine ID List"),
),
migrations.AlterField(
model_name="sharelink",
name="shash",
field=models.CharField(max_length=60, verbose_name="Link Key"),
),
migrations.AlterField(
model_name="sharelink",
name="uid",
field=models.CharField(max_length=16, verbose_name="User ID"),
),
migrations.AlterField(
model_name="userprofile",
name="deviceInfo",
field=models.TextField(blank=True, verbose_name="Login Information:"),
),
migrations.AlterField(
model_name="userprofile",
name="is_active",
field=models.BooleanField(default=True, verbose_name="Is Activated"),
),
migrations.AlterField(
model_name="userprofile",
name="is_admin",
field=models.BooleanField(default=False, verbose_name="Is Admin"),
),
migrations.AlterField(
model_name="userprofile",
name="username",
field=models.CharField(max_length=50, unique=True, verbose_name="Username"),
),
]

View File

@@ -0,0 +1,232 @@
# Generated by Django 4.2.7 on 2024-03-15 23:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0003_alter_rustdesdevice_options_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="rustdesdevice",
options={
"ordering": ("-rid",),
"verbose_name": "设备",
"verbose_name_plural": "设备列表",
},
),
migrations.AlterModelOptions(
name="rustdeskpeer",
options={
"ordering": ("-username",),
"verbose_name": "Peers",
"verbose_name_plural": "Peers列表",
},
),
migrations.AlterModelOptions(
name="rustdesktag",
options={
"ordering": ("-uid",),
"verbose_name": "Tags",
"verbose_name_plural": "Tags列表",
},
),
migrations.AlterModelOptions(
name="rustdesktoken",
options={
"ordering": ("-username",),
"verbose_name": "Token",
"verbose_name_plural": "Token列表",
},
),
migrations.AlterModelOptions(
name="sharelink",
options={
"ordering": ("-create_time",),
"verbose_name": "分享链接",
"verbose_name_plural": "链接列表",
},
),
migrations.AlterModelOptions(
name="userprofile",
options={
"permissions": (
("view_task", "Can see available tasks"),
("change_task_status", "Can change the status of tasks"),
("close_task", "Can remove a task by setting its status as closed"),
),
"verbose_name": "用户",
"verbose_name_plural": "用户列表",
},
),
migrations.AlterField(
model_name="rustdesdevice",
name="create_time",
field=models.DateTimeField(auto_now_add=True, verbose_name="设备注册时间"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="hostname",
field=models.CharField(max_length=100, verbose_name="主机名"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="memory",
field=models.CharField(max_length=100, verbose_name="内存"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="os",
field=models.CharField(max_length=100, verbose_name="操作系统"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="rid",
field=models.CharField(blank=True, max_length=60, verbose_name="客户端ID"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="username",
field=models.CharField(blank=True, max_length=100, verbose_name="系统用户名"),
),
migrations.AlterField(
model_name="rustdesdevice",
name="version",
field=models.CharField(max_length=100, verbose_name="客户端版本"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="alias",
field=models.CharField(max_length=30, verbose_name="别名"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="hostname",
field=models.CharField(max_length=30, verbose_name="操作系统名"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="platform",
field=models.CharField(max_length=30, verbose_name="平台"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="rhash",
field=models.CharField(max_length=60, verbose_name="设备链接密码"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="rid",
field=models.CharField(max_length=60, verbose_name="客户端ID"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="tags",
field=models.CharField(max_length=30, verbose_name="标签"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="uid",
field=models.CharField(max_length=16, verbose_name="用户ID"),
),
migrations.AlterField(
model_name="rustdeskpeer",
name="username",
field=models.CharField(max_length=20, verbose_name="系统用户名"),
),
migrations.AlterField(
model_name="rustdesktag",
name="tag_color",
field=models.CharField(blank=True, max_length=60, verbose_name="标签颜色"),
),
migrations.AlterField(
model_name="rustdesktag",
name="tag_name",
field=models.CharField(max_length=60, verbose_name="标签名称"),
),
migrations.AlterField(
model_name="rustdesktag",
name="uid",
field=models.CharField(max_length=16, verbose_name="所属用户ID"),
),
migrations.AlterField(
model_name="rustdesktoken",
name="access_token",
field=models.CharField(
blank=True, max_length=60, verbose_name="access_token"
),
),
migrations.AlterField(
model_name="rustdesktoken",
name="create_time",
field=models.DateTimeField(auto_now_add=True, verbose_name="登录时间"),
),
migrations.AlterField(
model_name="rustdesktoken",
name="uid",
field=models.CharField(max_length=16, verbose_name="用户ID"),
),
migrations.AlterField(
model_name="rustdesktoken",
name="username",
field=models.CharField(max_length=20, verbose_name="用户名"),
),
migrations.AlterField(
model_name="rustdesktoken",
name="uuid",
field=models.CharField(max_length=60, verbose_name="uuid"),
),
migrations.AlterField(
model_name="sharelink",
name="create_time",
field=models.DateTimeField(auto_now_add=True, verbose_name="生成时间"),
),
migrations.AlterField(
model_name="sharelink",
name="is_expired",
field=models.BooleanField(default=False, verbose_name="是否过期"),
),
migrations.AlterField(
model_name="sharelink",
name="is_used",
field=models.BooleanField(default=False, verbose_name="是否使用"),
),
migrations.AlterField(
model_name="sharelink",
name="peers",
field=models.CharField(max_length=20, verbose_name="机器ID列表"),
),
migrations.AlterField(
model_name="sharelink",
name="shash",
field=models.CharField(max_length=60, verbose_name="链接Key"),
),
migrations.AlterField(
model_name="sharelink",
name="uid",
field=models.CharField(max_length=16, verbose_name="用户ID"),
),
migrations.AlterField(
model_name="userprofile",
name="deviceInfo",
field=models.TextField(blank=True, verbose_name="登录信息:"),
),
migrations.AlterField(
model_name="userprofile",
name="is_active",
field=models.BooleanField(default=True, verbose_name="是否激活"),
),
migrations.AlterField(
model_name="userprofile",
name="is_admin",
field=models.BooleanField(default=False, verbose_name="是否管理员"),
),
migrations.AlterField(
model_name="userprofile",
name="username",
field=models.CharField(max_length=50, unique=True, verbose_name="用户名"),
),
]

View File

@@ -0,0 +1,253 @@
# Generated by Django 5.0.3 on 2024-10-28 10:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0004_alter_rustdesdevice_options_and_more'),
]
operations = [
migrations.CreateModel(
name='ConnLog',
fields=[
('id', models.IntegerField(primary_key=True, serialize=False, verbose_name='ID')),
('action', models.CharField(max_length=20, null=True, verbose_name='Action')),
('conn_id', models.CharField(max_length=10, null=True, verbose_name='Connection ID')),
('from_ip', models.CharField(max_length=30, null=True, verbose_name='From IP')),
('from_id', models.CharField(max_length=20, null=True, verbose_name='From ID')),
('rid', models.CharField(max_length=20, null=True, verbose_name='To ID')),
('conn_start', models.DateTimeField(null=True, verbose_name='Connected')),
('conn_end', models.DateTimeField(null=True, verbose_name='Disconnected')),
('session_id', models.CharField(max_length=60, null=True, verbose_name='Session ID')),
('uuid', models.CharField(max_length=60, null=True, verbose_name='UUID')),
],
),
migrations.CreateModel(
name='FileLog',
fields=[
('id', models.IntegerField(primary_key=True, serialize=False, verbose_name='ID')),
('file', models.CharField(max_length=500, verbose_name='Path')),
('remote_id', models.CharField(default='0', max_length=20, verbose_name='Remote ID')),
('user_id', models.CharField(default='0', max_length=20, verbose_name='User ID')),
('user_ip', models.CharField(default='0', max_length=20, verbose_name='User IP')),
('filesize', models.CharField(default='', max_length=500, verbose_name='Filesize')),
('direction', models.IntegerField(default=0, verbose_name='Direction')),
('logged_at', models.DateTimeField(null=True, verbose_name='Logged At')),
],
),
migrations.CreateModel(
name='GithubRun',
fields=[
('id', models.IntegerField(primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.CharField(max_length=100, verbose_name='uuid')),
('status', models.CharField(max_length=100, verbose_name='status')),
],
),
migrations.AlterModelOptions(
name='rustdesdevice',
options={'ordering': ('-rid',), 'verbose_name': 'Device', 'verbose_name_plural': 'Device List'},
),
migrations.AlterModelOptions(
name='rustdeskpeer',
options={'ordering': ('-username',), 'verbose_name': 'Peers', 'verbose_name_plural': 'Peers List'},
),
migrations.AlterModelOptions(
name='rustdesktag',
options={'ordering': ('-uid',), 'verbose_name': 'Tags', 'verbose_name_plural': 'Tags List'},
),
migrations.AlterModelOptions(
name='rustdesktoken',
options={'ordering': ('-username',), 'verbose_name': 'Token', 'verbose_name_plural': 'Token List'},
),
migrations.AlterModelOptions(
name='sharelink',
options={'ordering': ('-create_time',), 'verbose_name': 'Share Link', 'verbose_name_plural': 'Link List'},
),
migrations.AlterModelOptions(
name='userprofile',
options={'permissions': (('view_task', 'Can see available tasks'), ('change_task_status', 'Can change the status of tasks'), ('close_task', 'Can remove a task by setting its status as closed')), 'verbose_name': 'User', 'verbose_name_plural': 'User List'},
),
migrations.AddField(
model_name='rustdesdevice',
name='ip',
field=models.CharField(default='', max_length=16, verbose_name='IP Address'),
),
migrations.AddField(
model_name='rustdeskpeer',
name='ip',
field=models.CharField(blank=True, default='', max_length=16, verbose_name='IP Address'),
),
migrations.AlterField(
model_name='rustdesdevice',
name='create_time',
field=models.DateTimeField(auto_now_add=True, verbose_name='Device Registration Time'),
),
migrations.AlterField(
model_name='rustdesdevice',
name='hostname',
field=models.CharField(max_length=100, verbose_name='Hostname'),
),
migrations.AlterField(
model_name='rustdesdevice',
name='memory',
field=models.CharField(max_length=100, verbose_name='Memory'),
),
migrations.AlterField(
model_name='rustdesdevice',
name='os',
field=models.CharField(max_length=100, verbose_name='Operating System'),
),
migrations.AlterField(
model_name='rustdesdevice',
name='rid',
field=models.CharField(blank=True, max_length=60, verbose_name='Client ID'),
),
migrations.AlterField(
model_name='rustdesdevice',
name='update_time',
field=models.DateTimeField(auto_now=True, verbose_name='设备更新时间'),
),
migrations.AlterField(
model_name='rustdesdevice',
name='username',
field=models.CharField(blank=True, max_length=100, verbose_name='System Username'),
),
migrations.AlterField(
model_name='rustdesdevice',
name='version',
field=models.CharField(max_length=100, verbose_name='Client Version'),
),
migrations.AlterField(
model_name='rustdeskpeer',
name='alias',
field=models.CharField(max_length=30, verbose_name='Alias'),
),
migrations.AlterField(
model_name='rustdeskpeer',
name='hostname',
field=models.CharField(blank=True, max_length=30, verbose_name='Operating System Name'),
),
migrations.AlterField(
model_name='rustdeskpeer',
name='platform',
field=models.CharField(blank=True, max_length=30, verbose_name='Platform'),
),
migrations.AlterField(
model_name='rustdeskpeer',
name='rhash',
field=models.CharField(blank=True, max_length=60, verbose_name='Device Connection Password'),
),
migrations.AlterField(
model_name='rustdeskpeer',
name='rid',
field=models.CharField(max_length=60, verbose_name='Client ID'),
),
migrations.AlterField(
model_name='rustdeskpeer',
name='tags',
field=models.CharField(blank=True, max_length=30, verbose_name='Tag'),
),
migrations.AlterField(
model_name='rustdeskpeer',
name='uid',
field=models.CharField(max_length=16, verbose_name='User ID'),
),
migrations.AlterField(
model_name='rustdeskpeer',
name='username',
field=models.CharField(blank=True, max_length=20, verbose_name='System Username'),
),
migrations.AlterField(
model_name='rustdesktag',
name='tag_color',
field=models.CharField(blank=True, max_length=60, verbose_name='Tag Color'),
),
migrations.AlterField(
model_name='rustdesktag',
name='tag_name',
field=models.CharField(max_length=60, verbose_name='Tag Name'),
),
migrations.AlterField(
model_name='rustdesktag',
name='uid',
field=models.CharField(max_length=16, verbose_name='Belongs to User ID'),
),
migrations.AlterField(
model_name='rustdesktoken',
name='access_token',
field=models.CharField(blank=True, max_length=60, verbose_name='Access Token'),
),
migrations.AlterField(
model_name='rustdesktoken',
name='create_time',
field=models.DateTimeField(auto_now_add=True, verbose_name='Login Time'),
),
migrations.AlterField(
model_name='rustdesktoken',
name='uid',
field=models.CharField(max_length=16, verbose_name='User ID'),
),
migrations.AlterField(
model_name='rustdesktoken',
name='username',
field=models.CharField(max_length=20, verbose_name='Username'),
),
migrations.AlterField(
model_name='rustdesktoken',
name='uuid',
field=models.CharField(max_length=60, verbose_name='UUID'),
),
migrations.AlterField(
model_name='sharelink',
name='create_time',
field=models.DateTimeField(auto_now_add=True, verbose_name='Generation Time'),
),
migrations.AlterField(
model_name='sharelink',
name='is_expired',
field=models.BooleanField(default=False, verbose_name='Is Expired'),
),
migrations.AlterField(
model_name='sharelink',
name='is_used',
field=models.BooleanField(default=False, verbose_name='Is Used'),
),
migrations.AlterField(
model_name='sharelink',
name='peers',
field=models.CharField(max_length=20, verbose_name='Machine ID List'),
),
migrations.AlterField(
model_name='sharelink',
name='shash',
field=models.CharField(max_length=60, verbose_name='Link Key'),
),
migrations.AlterField(
model_name='sharelink',
name='uid',
field=models.CharField(max_length=16, verbose_name='User ID'),
),
migrations.AlterField(
model_name='userprofile',
name='deviceInfo',
field=models.TextField(blank=True, verbose_name='Login Information:'),
),
migrations.AlterField(
model_name='userprofile',
name='is_active',
field=models.BooleanField(default=True, verbose_name='Is Active'),
),
migrations.AlterField(
model_name='userprofile',
name='is_admin',
field=models.BooleanField(default=False, verbose_name='Is Administrator'),
),
migrations.AlterField(
model_name='userprofile',
name='username',
field=models.CharField(max_length=50, unique=True, verbose_name='Username'),
),
]

View File

2
api/models.py Normal file
View File

@@ -0,0 +1,2 @@
from .models_work import *
from .models_user import *

89
api/models_user.py Normal file
View File

@@ -0,0 +1,89 @@
# cython:language_level=3
from django.db import models
from django.contrib.auth.models import (
BaseUserManager,AbstractBaseUser,PermissionsMixin
)
from .models_work import *
from django.utils.translation import gettext as _
class MyUserManager(BaseUserManager):
def create_user(self, username, password=None):
if not username:
raise ValueError('Users must have an username')
user = self.model(username=username,
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, username, password):
user = self.create_user(username,
password=password,
)
user.is_admin = True
user.save(using=self._db)
return user
class UserProfile(AbstractBaseUser, PermissionsMixin):
username = models.CharField(_('用户名'),
unique=True,
max_length=50)
rid = models.CharField(verbose_name='RustDesk ID', max_length=16)
uuid = models.CharField(verbose_name='uuid', max_length=60)
autoLogin = models.BooleanField(verbose_name='autoLogin', default=True)
rtype = models.CharField(verbose_name='rtype', max_length=20)
deviceInfo = models.TextField(verbose_name=_('登录信息:'), blank=True)
is_active = models.BooleanField(verbose_name=_('是否激活'), default=True)
is_admin = models.BooleanField(verbose_name=_('是否管理员'), default=False)
objects = MyUserManager()
USERNAME_FIELD = 'username' # Fields used for household names
REQUIRED_FIELDS = ['password'] #The field that must be filled in
def get_full_name(self):
# The user is identified by their email address
return self.username
def get_short_name(self):
# The user is identified by their email address
return self.username
def __str__(self): # __unicode__ on Python 2
return self.username
def has_perm(self, perm, obj=None): #Is there any specified permission
"Does the user have a specific permission?"
# Simplest possible answer: Yes, always
return True
def has_module_perms(self, app_label):
"Does the user have permissions to view the app `app_label`?"
# Simplest possible answer: Yes, always
return True
@property
def is_staff(self):
"Is the user a member of staff?"
# Simplest possible answer: All admins are staff
return self.is_admin
class Meta:
verbose_name = _("用户")
verbose_name_plural = _("用户列表")
permissions = (
("view_task", "Can see available tasks"),
("change_task_status", "Can change the status of tasks"),
("close_task", "Can remove a task by setting its status as closed"),
)

151
api/models_work.py Normal file
View File

@@ -0,0 +1,151 @@
# cython:language_level=3
from django.db import models
from django.contrib import admin
from django.utils.translation import gettext as _
class RustDeskToken(models.Model):
''' Token
'''
username = models.CharField(verbose_name=_('用户名'), max_length=20)
rid = models.CharField(verbose_name=_('RustDesk ID'), max_length=16)
uid = models.CharField(verbose_name=_('用户ID'), max_length=16)
uuid = models.CharField(verbose_name=_('uuid'), max_length=60)
access_token = models.CharField(verbose_name=_('access_token'), max_length=60, blank=True)
create_time = models.DateTimeField(verbose_name=_('登录时间'), auto_now_add=True)
#expire_time = models.DateTimeField(verbose_name='过期时间')
class Meta:
ordering = ('-username',)
verbose_name = "Token"
verbose_name_plural = _("Token列表")
class ConnLog(models.Model):
id = models.IntegerField(verbose_name=_('ID'),primary_key=True)
action = models.CharField(verbose_name=_('Action'), max_length=20, null=True)
conn_id = models.CharField(verbose_name=_('Connection ID'), max_length=10, null=True)
from_ip = models.CharField(verbose_name=_('From IP'), max_length=30, null=True)
from_id = models.CharField(verbose_name=_('From ID'), max_length=20, null=True)
rid = models.CharField(verbose_name=_('To ID'), max_length=20, null=True)
conn_start = models.DateTimeField(verbose_name=_('Connected'), null=True)
conn_end = models.DateTimeField(verbose_name=_('Disconnected'), null=True)
session_id = models.CharField(verbose_name=_('Session ID'), max_length=60, null=True)
uuid = models.CharField(verbose_name=_('uuid'), max_length=60, null=True)
class ConnLogAdmin(admin.ModelAdmin):
list_display = ('id', 'action', 'conn_id', 'from_ip', 'from_id', 'rid', 'conn_start', 'conn_end', 'session_id', 'uuid')
search_fields = ('from_ip', 'rid')
list_filter = ('id', 'from_ip', 'from_id', 'rid', 'conn_start', 'conn_end')
class FileLog(models.Model):
id = models.IntegerField(verbose_name=_('ID'),primary_key=True)
file = models.CharField(verbose_name=_('Path'), max_length=500)
remote_id = models.CharField(verbose_name=_('Remote ID'), max_length=20, default='0')
user_id = models.CharField(verbose_name=_('User ID'), max_length=20, default='0')
user_ip = models.CharField(verbose_name=_('User IP'), max_length=20, default='0')
filesize = models.CharField(verbose_name=_('Filesize'), max_length=500, default='')
direction = models.IntegerField(verbose_name=_('Direction'), default=0)
logged_at = models.DateTimeField(verbose_name=_('Logged At'), null=True)
class FileLogAdmin(admin.ModelAdmin):
list_display = ('id', 'file', 'remote_id', 'user_id', 'user_ip', 'filesize', 'direction', 'logged_at')
search_fields = ('file', 'remote_id', 'user_id', 'user_ip')
list_filter = ('id', 'file', 'remote_id', 'user_id', 'user_ip', 'filesize', 'direction', 'logged_at')
class RustDeskTokenAdmin(admin.ModelAdmin):
list_display = ('username', 'uid')
search_fields = ('username', 'uid')
list_filter = ('create_time', ) #filter
class RustDeskTag(models.Model):
''' Tags
'''
uid = models.CharField(verbose_name=_('所属用户ID'), max_length=16)
tag_name = models.CharField(verbose_name=_('标签名称'), max_length=60)
tag_color = models.CharField(verbose_name=_('标签颜色'), max_length=60, blank=True)
class Meta:
ordering = ('-uid',)
verbose_name = "Tags"
verbose_name_plural = _("Tags列表")
class RustDeskTagAdmin(admin.ModelAdmin):
list_display = ('tag_name', 'uid', 'tag_color')
search_fields = ('tag_name', 'uid')
list_filter = ('uid', )
class RustDeskPeer(models.Model):
''' Pees
'''
uid = models.CharField(verbose_name=_('用户ID'), max_length=16)
rid = models.CharField(verbose_name=_('客户端ID'), max_length=60)
username = models.CharField(verbose_name=_('系统用户名'), max_length=20, blank=True)
hostname = models.CharField(verbose_name=_('操作系统名'), max_length=30, blank=True)
alias = models.CharField(verbose_name=_('别名'), max_length=30)
platform = models.CharField(verbose_name=_('平台'), max_length=30, blank=True)
tags = models.CharField(verbose_name=_('标签'), max_length=30, blank=True)
rhash = models.CharField(verbose_name=_('设备链接密码'), max_length=60, blank=True)
ip = models.CharField(verbose_name=_('IP Address'), max_length=16, default="", blank=True)
class Meta:
ordering = ('-username',)
verbose_name = "Peers"
verbose_name_plural = _("Peers列表" )
class RustDeskPeerAdmin(admin.ModelAdmin):
list_display = ('rid', 'uid', 'username', 'hostname', 'platform', 'alias', 'tags', 'ip')
search_fields = ('deviceid', 'alias')
list_filter = ('rid', 'uid', )
class RustDesDevice(models.Model):
rid = models.CharField(verbose_name=_('客户端ID'), max_length=60, blank=True)
cpu = models.CharField(verbose_name='CPU', max_length=100)
hostname = models.CharField(verbose_name=_('主机名'), max_length=100)
memory = models.CharField(verbose_name=_('内存'), max_length=100)
os = models.CharField(verbose_name=_('操作系统'), max_length=100)
uuid = models.CharField(verbose_name='uuid', max_length=100)
username = models.CharField(verbose_name=_('系统用户名'), max_length=100, blank=True)
version = models.CharField(verbose_name=_('客户端版本'), max_length=100)
create_time = models.DateTimeField(verbose_name=_('设备注册时间'), auto_now_add=True)
update_time = models.DateTimeField(verbose_name=('设备更新时间'), auto_now=True, blank=True)
ip = models.CharField(verbose_name=_('IP Address'), max_length=16, default="")
class Meta:
ordering = ('-rid',)
verbose_name = _("设备")
verbose_name_plural = _("设备列表" )
class RustDesDeviceAdmin(admin.ModelAdmin):
list_display = ('rid', 'hostname', 'memory', 'uuid', 'version', 'create_time', 'update_time', 'ip')
search_fields = ('hostname', 'memory')
list_filter = ('rid', )
class ShareLink(models.Model):
''' Share link
'''
uid = models.CharField(verbose_name=_('用户ID'), max_length=16)
shash = models.CharField(verbose_name=_('链接Key'), max_length=60)
peers = models.CharField(verbose_name=_('机器ID列表'), max_length=20)
is_used = models.BooleanField(verbose_name=_('是否使用'), default=False)
is_expired = models.BooleanField(verbose_name=_('是否过期'), default=False)
create_time = models.DateTimeField(verbose_name=_('生成时间'), auto_now_add=True)
class Meta:
ordering = ('-create_time',)
verbose_name = _("分享链接")
verbose_name_plural = _("链接列表" )
class ShareLinkAdmin(admin.ModelAdmin):
list_display = ('shash', 'uid', 'peers', 'is_used', 'is_expired', 'create_time')
search_fields = ('peers', )
list_filter = ('is_used', 'uid', 'is_expired' )
class GithubRun(models.Model):
id = models.IntegerField(verbose_name="ID",primary_key=True)
uuid = models.CharField(verbose_name="uuid", max_length=100)
status = models.CharField(verbose_name="status", max_length=100)

View File

@@ -0,0 +1,28 @@
{% extends phone_or_desktop %}
{% load my_filters %}
{% block title %}RustDesk{% endblock %}
{% block legend_name %}{{ "Add Peer" | translate }}{% endblock %}
{% block content %}
<div style="padding: 20px; background-color: #F2F2F2;">
<div class="layui-row layui-col-space15">
<div class="layui-col-md15">
<div class="layui-card">
<div class="layui-card-header">{{ "Add Client" }}</div>
<div class="layui-card-body">
<form action="/api/add_peer" method="post" enctype="multipart/form-data">
<label for="{{ form.clientID.id_for_label }}">Rustdesk client ID:</label>
{{ form.clientID }}<br><br>
<label for="{{ form.alias.id_for_label }}">Alias:</label>
{{ form.alias }}<br><br>
<label for="{{ form.tags.id_for_label }}">Tags:</label>
{{ form.tags }}<br><br>
<button type="submit">Add Client</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.getElementById("{{ form.clientID.id_for_label }}").value = "{{rid}}"
</script>
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% extends phone_or_desktop %}
{% load my_filters %}
{% block title %}RustDesk{% endblock %}
{% block legend_name %}{{ "Add Peer" | translate }}{% endblock %}
{% block content %}
<div style="padding: 20px; background-color: #F2F2F2;">
<div class="layui-row layui-col-space15">
<div class="layui-col-md15">
<div class="layui-card">
<div class="layui-card-header">{{ "Assign Client to User" }}</div>
<div class="layui-card-body">
<form action="/api/assign_peer" method="post" enctype="multipart/form-data">
{{ form.uid }}<br><br>
<label for="{{ form.clientID.id_for_label }}">Rustdesk client ID:</label>
{{ form.clientID }}<br><br>
<label for="{{ form.alias.id_for_label }}">Alias:</label>
{{ form.alias }}<br><br>
<label for="{{ form.tags.id_for_label }}">Tags:</label>
{{ form.tags }}<br><br>
<button type="submit">Assign Client</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.getElementById("{{ form.clientID.id_for_label }}").value = "{{rid}}"
</script>
{% endblock %}

75
api/templates/base.html Normal file
View File

@@ -0,0 +1,75 @@
{% load static %}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock %}</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="{% static 'layui/css/layui.css' %}">
{% block link %}{% endblock %}
</head>
<body>
<script src={% static "layui/layui.js" %}></script>
<script>
layui.use('element', function () {
var element = layui.element; //导航的hover效果、二级菜单等功能需要依赖element模块
//监听导航点击
element.on('nav(demo)', function (elem) {
//console.log(elem)
layer.msg(elem.text());
});
});
document.addEventListener('DOMContentLoaded', (event) => {
// Place your globalSearch function or any DOM-related JS here
window.globalSearch = function() {
var input, filter, tables, tr, td, txtValue;
input = document.getElementById("globalSearchInput");
filter = input.value.toUpperCase();
tables = document.getElementsByTagName("table");
for (let table of tables) {
tr = table.getElementsByTagName("tr");
for (let i = 1; i < tr.length; i++) {
let row = tr[i];
let cells = row.getElementsByTagName("td");
let textContent = Array.from(cells).map(cell => cell.textContent.toUpperCase());
row.style.display = textContent.some(text => text.includes(filter)) ? "" : "none";
}
}
}
});
</script>
<ul class="layui-nav">
<li class="layui-nav-item"><a href="/">User Devices</a></li>
{% if u.is_admin %}
<li class="layui-nav-item"><a href="/api/work?show_type=admin">{% trans "所有设备" %}</a>
</li>
{% endif %}
<!-- <li class="layui-nav-item"><a href="/api/share">{% trans "分享" %}</a></li> -->
<li class="layui-nav-item"><a href="/webui" target="_blank">{% trans "网页控制" %}</a></li>
{% if u.is_admin %}
<li class="layui-nav-item"><a href="/admin" target="_blank">{% trans "管理后台" %}</a>
</li>
<li class="layui-nav-item"><a href="/api/conn_log">Connection Log</a></li>
<li class="layui-nav-item"><a href="/api/file_log">File Transfer Log</a></li>
<li class="layui-nav-item"><a href="/api/sys_info">Server Information</a></li>
<li class="layui-nav-item"><a href="/api/clients">Client Downloads</a></li>
{% endif %}
<li class="layui-nav-item"><a href="/api/user_action?action=logout" target="_blank">{% trans "退出" %}</a></li>
</ul>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
<legend>{% block legend_name %}{% endblock %}</legend>
</fieldset>
{% block content %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,84 @@
{% load static %}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock %}</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="{% static 'layui/css/layui.css' %}">
{% block link %}{% endblock %}
</head>
<body>
<script src={% static "layui/layui.js" %}></script>
<script>
layui.use('element', function () {
var element = layui.element; //导航的hover效果、二级菜单等功能需要依赖element模块
//监听导航点击
element.on('nav(demo)', function (elem) {
//console.log(elem)
layer.msg(elem.text());
});
});
document.addEventListener('DOMContentLoaded', (event) => {
// Place your globalSearch function or any DOM-related JS here
window.globalSearch = function() {
var input, filter, tables, tr, td, txtValue;
input = document.getElementById("globalSearchInput");
filter = input.value.toUpperCase();
tables = document.getElementsByTagName("table");
for (let table of tables) {
tr = table.getElementsByTagName("tr");
for (let i = 1; i < tr.length; i++) {
let row = tr[i];
let cells = row.getElementsByTagName("td");
let textContent = Array.from(cells).map(cell => cell.textContent.toUpperCase());
row.style.display = textContent.some(text => text.includes(filter)) ? "" : "none";
}
}
}
});
</script>
<div id="container">
<nav role="navigation">
<div id="menuToggle">
<input type="checkbox" />
<span></span>
<span></span>
<span></span>
<ul id="menu">
<li class="layui-nav-item"><a href="/">User Devices</a></li>
{% if u.is_admin %}
<li class="layui-nav-item"><a href="/api/work?show_type=admin">{% trans "所有设备" %}</a>
</li>
{% endif %}
<!-- <li class="layui-nav-item"><a href="/api/share">{% trans "分享" %}</a></li> -->
<li class="layui-nav-item"><a href="/webui" target="_blank">{% trans "网页控制" %}</a></li>
{% if u.is_admin %}
<li class="layui-nav-item"><a href="/admin" target="_blank">{% trans "管理后台" %}</a>
</li>
<li class="layui-nav-item"><a href="/api/conn_log">Connection Log</a></li>
<li class="layui-nav-item"><a href="/api/file_log">File Transfer Log</a></li>
<li class="layui-nav-item"><a href="/api/sys_info">Server Information</a></li>
<li class="layui-nav-item"><a href="/api/clients">Client Downloads</a></li>
{% endif %}
<li class="layui-nav-item"><a href="/api/user_action?action=logout" target="_blank">{% trans "退出" %}</a></li>
</ul>
</div>
</nav>
</div>
<br>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
<legend>{% block legend_name %}{% endblock %}</legend>
</fieldset>
{% block content %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,89 @@
{% extends phone_or_desktop %}
{% load my_filters %}
{% block title %}RustDesk WebUI{% endblock %}
{% block legend_name %}{{ "Client Downloads" | translate }}{% endblock %}
{% load static %}
{% block content %}
<script src="{% static 'js/sorttable.js' %}"></script>
<style>
.container {
display: grid;
grid-template-columns: 50% 50%;
grid-template-rows: repeat(2, auto); /* Adjust the number of rows as needed */
padding: 20px; /* Adjust the padding value as needed */
max-width: 1000px; /* Set a maximum width for the container */
margin: 0 auto; /* Center the container horizontally */
}
.section {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 20px;
margin-left: 10px;
margin-right: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Add a subtle box shadow */
border-radius: 5px; /* Add rounded corners for a more 3D effect */
}
</style>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
// Place your globalSearch function or any DOM-related JS here
window.globalSearch = function() {
var input, filter, tables, tr, td, txtValue;
input = document.getElementById("globalSearchInput");
filter = input.value.toUpperCase();
tables = document.getElementsByTagName("table");
for (let table of tables) {
tr = table.getElementsByTagName("tr");
for (let i = 1; i < tr.length; i++) {
let row = tr[i];
let cells = row.getElementsByTagName("td");
let textContent = Array.from(cells).map(cell => cell.textContent.toUpperCase());
row.style.display = textContent.some(text => text.includes(filter)) ? "" : "none";
}
}
}
});
</script>
<input type="text" id="globalSearchInput" onkeyup="globalSearch()" placeholder="Search all fields..." style="width: 200px; height: 20px; margin-bottom: 10px;">
<a href="/api/generator">Client Generator</a>
<div class="container">
<div class="section">
<h1>Github Clients</h1>
<table class="sortable">
<thead></thead>
<tbody>
<tr>
<th>File</th>
<th>Date</th>
</tr>
{% for filename, fileinfo in client_files.items %}
<tr>
<td><a href='/api/download?filename={{filename}}&path={{fileinfo.path}}'>{{filename}}</a></td>
<td>{{fileinfo.modified}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="section">
<h1>Custom Clients</h1>
<table class="sortable">
<thead></thead>
<tbody>
<tr>
<th>File</th>
<th>Date</th>
</tr>
{% for filename, fileinfo in client_custom_files.items %}
<tr>
<td><a href='/api/download?filename={{filename}}&path={{fileinfo.path}}'>{{filename}}</a></td>
<td>{{fileinfo.modified}}</td>
<td><a href='/api/delete_file?filename={{filename}}&path={{fileinfo.path}}'>Delete</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,32 @@
{% extends phone_or_desktop %}
{% load my_filters %}
{% block title %}RustDesk{% endblock %}
{% block legend_name %}{{ "Edit Peer" | translate }}{% endblock %}
{% block content %}
{{peer.rid}}
<div style="padding: 20px; background-color: #F2F2F2;">
<div class="layui-row layui-col-space15">
<div class="layui-col-md15">
<div class="layui-card">
<div class="layui-card-header">{{ "Edit Peer" }} {{ peer.rid }}</div>
<div class="layui-card-body">
<form action="/api/edit_peer" method="post" enctype="multipart/form-data">
<label for="{{ form.clientID.id_for_label }}">Client Rustdesk ID:</label>
{{ form.clientID }}<br><br>
<label for="{{ form.alias.id_for_label }}">Alias:</label>
{{ form.alias }}<br><br>
<label for="{{ form.tags.id_for_label }}">Tags:</label>
{{ form.tags }}<br><br>
<label for="{{ form.username.id_for_label }}">System Username:</label>
{{ form.username }}<br><br>
<label for="{{ form.hostname.id_for_label }}">Computer Name:</label>
{{ form.hostname }}<br><br>
<label for="{{ form.platform.id_for_label }}">Platform:</label>
{{ form.platform }}<br><br>
<button type="submit">Save Client</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,7 @@
{% extends phone_or_desktop %}
{% load my_filters %}
{% block title %}RustDesk WebUI{% endblock %}
{% block legend_name %}{{ "Client Generator" | translate }}{% endblock %}
{% block content %}
<a href='/api/download_client?filename={{filename}}&uuid={{uuid}}'>{{filename}}</a>
{% endblock %}

View File

@@ -0,0 +1,253 @@
{% extends phone_or_desktop %}
{% load my_filters %}
{% block title %}RustDesk WebUI{% endblock %}
{% block legend_name %}{{ "Client Generator" | translate }}{% endblock %}
{% block content %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Roboto', sans-serif;
background-color: #000;
color: #e0e0e0;
margin: 0;
padding: 20px;
}
.platform {
display: grid;
grid-template-columns: 1fr;
grid-gap: 20px;
margin: 0 auto;
padding: 20px;
max-width: 1200px;
}
.container {
display: grid;
grid-template-columns: 1fr 1fr; /* Adjust as needed */
grid-gap: 20px;
margin: 0 auto; /* Center the container horizontally */
padding: 20px;
max-width: 1200px;
}
.column {
flex: 50%;
}
h1 {
color: #fff;
text-align: center;
grid-column: 1 / -1;
}
h2 {
color: #fff;
margin-top: 0;
}
.section {
background-color: #111;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
flex: 50%;
}
label {
display: block;
margin-bottom: 5px;
color: #bbb;
}
input[type="text"], input[type="password"], select, textarea {
width: 100%;
padding: 8px;
margin-bottom: 10px;
background-color: #222;
border: 1px solid #444;
border-radius: 4px;
color: #fff;
}
input[type="radio"], input[type="checkbox"] {
margin-right: 5px;
}
button {
background-color: #0077ff;
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #0066cc;
}
.platform-icons {
display: flex;
justify-content: space-around;
margin-bottom: 20px;
}
.platform-icon {
font-size: 32px;
color: #bbb;
cursor: pointer;
transition: color 0.3s ease;
}
.platform-icon:hover, .platform-icon.active {
color: #fff;
}
.checkbox-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.checkbox-group label {
display: flex;
align-items: center;
}
.preview-image {
max-width: 100%;
max-height: 100px;
margin-top: 10px;
}
</style>
</head>
<body>
<h1><i class="fas fa-cogs"></i> RustDesk Custom Client Builder</h1>
<form action="/api/generator" method="post" enctype="multipart/form-data">
<div class="platform">
<h2><i class="fas fa-desktop"></i> Select Platform</h2>
<div class="platform-icons">
<i class="fab fa-windows platform-icon active" data-platform="windows"></i>
<i class="fab fa-linux platform-icon" data-platform="linux"></i>
<i class="fab fa-android platform-icon" data-platform="android"></i>
</div>
<select name="platform" id="id_platform">
<option value="windows" selected>Windows</option>
<option value="linux">Linux</option>
<option value="android">Android</option>
</select>
<label for="{{ form.version.id_for_label }}">Rustdesk Version:</label>
{{ form.version }}
<label for="{{ form.delayFix.id_for_label }}">{{ form.delayFix }} Fix connection delay when using third-party API</label>
</div>
</div>
<div class="container">
<div class="section">
<h2><i class="fas fa-sliders-h"></i> General</h2>
<label for="{{ form.exename.id_for_label }}">Name of the configuration:</label>
{{ form.exename }}<br><br>
<label for="{{ form.appname.id_for_label }}">Custom Application Name:</label>
{{ form.appname }}<br><br>
<label for="{{ form.direction.id_for_label }}">Connection Type:</label>
{{ form.direction }}<br><br>
<label for="{{ form.installation.id_for_label }}">Disable Installation:</label>
{{ form.installation }}<br><br>
<label for="{{ form.settings.id_for_label }}">Disable Settings:</label>
{{ form.settings }}<br><br>
</div>
<div class="section">
<h2><i class="fas fa-server"></i> Custom Server</h2>
<label for="{{ form.serverIP.id_for_label }}">Host:</label>
{{ form.serverIP }}<br><br>
<label for="{{ form.key.id_for_label }}">Key:</label>
{{ form.key }}<br><br>
<label for="{{ form.apiServer.id_for_label }}">API:</label>
{{ form.apiServer }}<br><br>
<label for="{{ form.urlLink.id_for_label }}">Custom URL for links (replaces https://rustdesk.com):</label>
{{ form.urlLink }}<br><br>
</div>
</div>
<div class="container">
<div class="section">
<h2><i class="fas fa-shield-alt"></i> Security</h2>
<label for="{{ form.runasadmin.id_for_label }}">Always run as Administrator?</label>
{{ form.runasadmin }}<br><br>
<label for="{{ form.passApproveMode.id_for_label }}">Password Approve mode:</label>
{{ form.passApproveMode }}<br><br>
<label for="{{ form.permanentPassword.id_for_label }}">Set Permanent Password:</label>
{{ form.permanentPassword }} *The password is used as default, but can be changed by the client<br><br>
<label for="{{ form.denyLan.id_for_label }}">{{ form.denyLan }} Deny LAN discovery</label><br>
<label for="{{ form.enableDirectIP.id_for_label }}">{{ form.enableDirectIP }} Enable direct IP access</label><br>
<label for="{{ form.autoClose.id_for_label }}">{{ form.autoClose }} Automatically close incoming sessions on user inactivity</label><br>
</div>
<div class="section">
<h2><i class="fas fa-paint-brush"></i> Visual</h2>
<label for="{{ form.iconfile.id_for_label }}">Custom App Icon (in .png format)</label>
{{ form.iconfile }}<br><br>
<!-- <input type="file" name="iconfile" id="iconfile" accept="image/png"> -->
<div id="icon-preview"></div><br><br>
<label for="{{ form.logofile.id_for_label }}">Custom App Logo (in .png format)</label>
{{ form.logofile }}<br><br>
<!-- <input type="file" name="logofile" id="logofile" accept="image/png"> -->
<div id="logo-preview"></div><br><br>
<label for="{{ form.theme.id_for_label }}">Theme:</label>
{{ form.theme }} {{ form.themeDorO }} *Default sets the theme but allows the client to change it, Override sets the theme permanently.<br><br>
</div>
</div>
<div class="container">
<div class="section">
<h2><i class="fas fa-lock"></i> Permissions</h2>
The following Permissions can be set as default (the user can change the settins) or override (the settings cannot be changed).<br>
{{ form.permissionsDorO }}
<label for="{{ form.permissionsType.id_for_label }}">Permission type:</label>
{{ form.permissionsType }}<br><br>
<div class="checkbox-group">
<label for="{{ form.enableKeyboard.id_for_label }}">{{ form.enableKeyboard }} Enable keyboard/mouse</label>
<label for="{{ form.enableClipboard.id_for_label }}">{{ form.enableClipboard }} Enable clipboard</label>
<label for="{{ form.enableFileTransfer.id_for_label }}">{{ form.enableFileTransfer }} Enable file transfer</label>
<label for="{{ form.enableAudio.id_for_label }}">{{ form.enableAudio }} Enable audio</label>
<label for="{{ form.enableTCP.id_for_label }}">{{ form.enableTCP }} Enable TCP tunneling</label>
<label for="{{ form.enableRemoteRestart.id_for_label }}">{{ form.enableRemoteRestart }} Enable remote restart</label>
<label for="{{ form.enableRecording.id_for_label }}">{{ form.enableRecording }} Enable recording session</label>
<label for="{{ form.enableBlockingInput.id_for_label }}">{{ form.enableBlockingInput }} Enable blocking user input</label>
<label for="{{ form.enableRemoteModi.id_for_label }}">{{ form.enableRemoteModi }} Enable remote configuration modification</label>
</div>
</div>
<div class="section">
<h2><i class="fas fa-cog"></i> Other</h2>
<label for="{{ form.removeWallpaper.id_for_label }}">{{ form.removeWallpaper }} Remove wallpaper during incoming sessions</label><br>
<label for="{{ form.defaultManual.id_for_label }}">Default settings</label><br>
{{ form.defaultManual }}<br><br>
<label for="{{ form.overrideManual.id_for_label }}">Override settings</label><br>
{{ form.overrideManual }}<br><br>
</div>
</div>
<div class="platform">
<div class="section">
<button type="submit"><i class="fas fa-rocket"></i> Generate Custom Client</button>
</div>
</div>
</form>
<script>
document.querySelectorAll('.platform-icon').forEach(icon => {
icon.addEventListener('click', function() {
document.querySelectorAll('.platform-icon').forEach(i => i.classList.remove('active'));
this.classList.add('active');
document.getElementById('id_platform').value = this.dataset.platform;
});
});
document.getElementById("{{ form.iconfile.id_for_label }}").addEventListener('change', function(event) {
previewImage(event.target, 'icon-preview');
});
document.getElementById("{{ form.logofile.id_for_label }}").addEventListener('change', function(event) {
previewImage(event.target, 'logo-preview');
});
function previewImage(input, previewContainerId) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
var img = document.createElement('img');
img.src = e.target.result;  
img.style.maxWidth = '300px';
img.style.maxHeight = '60px';
document.getElementById(previewContainerId).innerHTML = '';
document.getElementById(previewContainerId).appendChild(img);
};
reader.readAsDataURL(input.files[0]);
}
}
</script>
{% endblock %}

70
api/templates/login.html Normal file
View File

@@ -0,0 +1,70 @@
{% load static %}
{% load my_filters %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ "登录" | translate }}_【RustDeskWeb】</title>
<link rel="stylesheet" href="{% static 'layui/css/layui.css' %}">
<link rel="stylesheet" href="{% static 'layui/css/style.css' %}">
</head>
<body>
<div class="login-main">
<header class="layui-elip">{{ "登录" | translate }}</header>
<form class="layui-form">
<div class="layui-input-inline">
<input type="text" name="account" required lay-verify="required" placeholder="{{ "用户名" | translate }}" autocomplete="off"
class="layui-input">
</div>
<div class="layui-input-inline">
<input type="password" name="password" required lay-verify="required" placeholder="{{ "密码" | translate }}" autocomplete="off"
class="layui-input">
</div>
<div class="layui-input-inline login-btn">
<button lay-submit lay-filter="login" class="layui-btn">{{ "登录" | translate }}</button>
</div>
<hr/>
<p><a href="/api/user_action?action=register" class="fl">{{ "立即注册" | translate }}</a><a href="javascript:;" class="fr">{{ "忘记密码?" | translate }}</a></p>
</form>
</div>
<script src={% static "layui/layui.js" %}></script>
<script type="text/javascript">
layui.use(['form','layer','jquery'], function () {
// 操作对象
var form = layui.form;
var $ = layui.jquery;
form.on('submit(login)',function (data) {
console.log(data.field);
$.ajax({
url:'/api/user_action?action=login',
data:data.field,
dataType:'json',
type:'post',
success: function(resp) {
if(resp.code==1) {
//layer.alert(resp.msg,{icon:1});
location.href = resp.url;
} else {
layer.alert(resp.msg,{icon:5});
}
}
})
return false;
})
$('.fr').on('click', function(){
layer.alert("{{ "这么简易的东西忘记密码这功能就没必要了吧" | translate }}",{icon:5});
})
});
</script>
</body>
</html>

13
api/templates/msg.html Normal file
View File

@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% load my_filters %}
{% block title %}{{title}}{% endblock %}
{% block legend_name %}{{ "信息" | translate }}{% endblock %}
{% block content %}
<div style="padding: 20px; background-color: #F2F2F2;">
<div class="layui-row layui-col-space15">
{% autoescape off %}
{{msg}}
{% endautoescape %}
</div></div>
{% endblock %}

144
api/templates/reg.html Normal file
View File

@@ -0,0 +1,144 @@
{% load static %}
{% load my_filters %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{{ "注册" | translate }}_【RustDeskWeb】</title>
<link rel="stylesheet" href="{% static 'layui/css/layui.css' %}">
<link rel="stylesheet" href="{% static 'layui/css/style.css' %}">
<link rel="icon" href="../frame/static/image/code.png">
</head>
<body>
<div class="login-main">
<header class="layui-elip" style="width: 82%">{{ "注册页" | translate }}</header>
<!-- 表单选项 -->
<form class="layui-form">
<div class="layui-input-inline">
<!-- 用户名 -->
<div class="layui-inline" style="width: 85%">
<input type="text" id="user" name="account" required lay-verify="required" placeholder="{{ "请输入用户名" | translate }}" autocomplete="off" class="layui-input">
</div>
<!-- 对号 -->
<div class="layui-inline">
<i class="layui-icon" id="ri" style="color: green;font-weight: bolder;" hidden></i>
</div>
<!-- 错号 -->
<div class="layui-inline">
<i class="layui-icon" id="wr" style="color: red; font-weight: bolder;" hidden></i>
</div>
</div>
<!-- 密码 -->
<div class="layui-input-inline">
<div class="layui-inline" style="width: 85%">
<input type="password" id="pwd" name="password" required lay-verify="required" placeholder="{{ "请输入密码" | translate }}" autocomplete="off" class="layui-input">
</div>
<!-- Signs -->
<div class="layui-inline">
<i class="layui-icon" id="pri" style="color: green;font-weight: bolder;" hidden></i>
</div>
<!-- Wrong number -->
<div class="layui-inline">
<i class="layui-icon" id="pwr" style="color: red; font-weight: bolder;" hidden>Gauge</i>
</div>
</div>
<!-- 确认密码 -->
<div class="layui-input-inline">
<div class="layui-inline" style="width: 85%">
<input type="password" id="rpwd" name="repassword" required lay-verify="required" placeholder="{{ "请确认密码" | translate }}" autocomplete="off" class="layui-input">
</div>
<!-- 对号 -->
<div class="layui-inline">
<i class="layui-icon" id="rpri" style="color: green;font-weight: bolder;" hidden></i>
</div>
<!-- 错号 -->
<div class="layui-inline">
<i class="layui-icon" id="rpwr" style="color: red; font-weight: bolder;" hidden></i>
</div>
</div>
<div class="layui-input-inline login-btn" style="width: 85%">
<button type="submit" lay-submit lay-filter="sub" class="layui-btn">{{ "注册" | translate }}</button>
</div>
<hr style="width: 85%" />
<p style="width: 85%"><a href="/api/user_action?action=login" class="fl">{{ "已有账号?立即登录" | translate }}</a></p>
</form>
</div>
<script src={% static "layui/layui.js" %}></script>
<script type="text/javascript">
layui.use(['form','jquery','layer'], function () {
var form = layui.form;
var $ = layui.jquery;
var layer = layui.layer;
//添加表单失焦事件
//验证表单
$('#user').blur(function() {
var user = $(this).val();
});
// you code ...
// 为密码添加正则验证
$('#pwd').blur(function() {
var reg = /^[\w\S]{8,20}$/;
if(!($('#pwd').val().match(reg))){
//layer.msg('请输入合法密码');
$('#pwr').removeAttr('hidden');
$('#pri').attr('hidden','hidden');
layer.msg('{{ "请输入8~20位密码。可以包含字母、数字和特殊字符。" | translate }}');
}else {
$('#pri').removeAttr('hidden');
$('#pwr').attr('hidden','hidden');
}
});
//验证两次密码是否一致
$('#rpwd').blur(function() {
if($('#pwd').val() != $('#rpwd').val()){
$('#rpwr').removeAttr('hidden');
$('#rpri').attr('hidden','hidden');
layer.msg('{{ "两次输入密码不一致!" | translate }}');
}else {
$('#rpri').removeAttr('hidden');
$('#rpwr').attr('hidden','hidden');
};
});
//
//添加表单监听事件,提交注册信息
form.on('submit(sub)', function() {
$.ajax({
url:'/api/user_action?action=register',
type:'post',
dataType:'json',
data:{
user:$('#user').val(),
pwd:$('#pwd').val(),
},
success:function(data){
if (data.code == 1) {
layer.msg('{{ "注册成功,请前往登录页登录。" | translate }}');
///location.href = "login.html";
}else {
layer.msg(data.msg);
}
}
})
//防止页面跳转
return false;
});
});
</script>
</body>
</html>

106
api/templates/share.html Normal file
View File

@@ -0,0 +1,106 @@
{% extends "base.html" %}{% load static %}
{% load my_filters %}
{% block title %}{{ "分享机器" | translate }}{% endblock %}
{% block link %}<link rel="stylesheet" href="{% static 'layui/css/style.css' %}">{% endblock %}
{% block legend_name %}{{ "分享机器给其他用户" | translate }}{% endblock %}
{% block content %}
<div class="layui-container">
<div class="layui-card layui-col-md3-offset2">
<div class="layui-card-header">{{ "请将要分享的机器调整到右侧" | translate }}</div>
<div id="showdevice"></div>
<button id="create" type="button" class="layui-btn padding-5" lay-on="getData">{{ "生成分享链接" | translate }}</button>
</div>
<div class="layui-card">{{ "1、链接有效期为15分钟切勿随意分享给他人。" | translate }}</div>
<div class="layui-card">{{ "2、所分享的机器被分享人享有相同的权限如果机器设置了保存密码被分享人也可以直接连接。" | translate }}</div>
<div class="layui-card">{{ "3、为保障安全链接有效期为15分钟、链接仅有效1次。链接一旦被非分享人的登录用户访问分享生效后续访问链接失效。" | translate }}</div>
<div class="layui-card layui-col-md6-offset1">
<table class="layui-table">
<colgroup>
<col width="30">
<col width="150">
<col width="200">
<col>
</colgroup>
<thead>
<tr>
<th>{{ "链接地址" | translate }}</th>
<th>{{ "创建时间" | translate }}</th>
<th>{{ "ID列表" | translate }}</th>
</tr>
</thead>
<tbody>
{% for one in sharelinks %}
<tr>
<td><script> document.write(window.location);</script>/{{one.shash}} </td>
<td>{{one.create_time}} </td>
<td>{{one.peers}} </td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script>
layui.use(['transfer', 'jquery', 'layer'], function(){
var transfer = layui.transfer;
var $ = layui.jquery;
var layer = layui.layer;
//渲染
transfer.render({
elem: '#showdevice' //绑定元素
,title: ['{{ "我的机器" | translate }}', '{{ "分享机器" | translate }}'] //自定义标题
//,width: 500 //定义宽度
//,height: 300 //定义高度
,data: [//定义数据源
{%for peer in peers %}
{"value": "{{peer.id}}", "title": "{{peer.name}}"},
{%endfor%}
] //disabled Whether to disable checked Whether to choose
,id: 'device' //Define indexes You can use it when reloading RELOAD or getting the right data
});
$("#create_bak").click(function(){
var getData = transfer.getData('device');
alert(JSON.stringify(getData));
});
$("#create").click(function(){
var getData = transfer.getData('device');
$.ajax({
url:'/api/share',
type:'post',
dataType:'json',
data:{
data:JSON.stringify(getData),
},
success:function(data){
if (data.code == 1) {
// var myMsg = layer.msg('处理中', {
// shade: 0.4,
// time:false //取消自动关闭
// });
//layer.msg('注册成功,请前往登录页登录。');
layer.alert('{{ "成功!如需分享,请复制以下链接给其他人:<br>" | translate }}'+ window.location + '/' +data.shash, function (index) {
location.reload();});
}else {
layer.msg(data.msg);
}
}
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,66 @@
{% extends phone_or_desktop %}
{% load my_filters %}
{% block title %}RustDesk WebUI{% endblock %}
{% block legend_name %}{{ "Connection Log" | translate }}{% endblock %}
{% load static %}
{% block content %}
<script src="{% static 'js/sorttable.js' %}"></script>
<div style="padding: 20px; background-color: #F2F2F2;">
<div class="layui-row layui-col-space15">
<div class="layui-col-md15">
<input type="text" id="globalSearchInput" onkeyup="globalSearch()" placeholder="Search all fields..." style="width: 200px; height: 20px; margin-bottom: 10px;">
<div class="layui-card">
<div class="layui-card-header">{{ "Connection Log" }}:【{{u.username}}】</div>
<div class="layui-card-body">
<table class="layui-table sortable">
<thead>
<tr>
<th>User IP</th>
<th>User ID</th>
<th>User Alias</th>
<th>Remote ID</th>
<th>Remote Alias</th>
<th>Connection Start Time</th>
<th>Connection End Time</th>
<th>Duration (HH:MM:SS)</th>
</tr>
</thead>
<tbody>
{% for one in page_obj %}
<tr>
<td>{{one.from_ip}}</td>
<td>{{one.from_id}}</td>
<td>{{one.from_alias}}</td>
<td>{{one.rid}}</td>
<td>{{one.alias}}</td>
<td>{{one.conn_start}}</td>
<td>{{one.conn_end}}</td>
<td>{{one.duration}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-md4 layui-col-md-offset4">
<span class="step-links">
{% if page_obj.has_previous %}
<button class="layui-btn" ><a href="?page=1">&laquo; {{ "首页" | translate }}</a></button>
<button class="layui-btn" ><a href="?page={{ page_obj.previous_page_number }}">{{ "上一页" | translate }}</a></button>
{% endif %}
{% if page_obj.paginator.num_pages > 1 %}
<span class="current">
{{ "页码" | translate }} {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
</span>
{% endif %}
{% if page_obj.has_next %}
<button class="layui-btn" > <a href="?page={{ page_obj.next_page_number }}">{{ "下一页" | translate }}</a></button>
<button class="layui-btn" ><a href="?page={{ page_obj.paginator.num_pages }}">{{ "尾页" | translate }} &raquo;</a></button>
{% endif %}
</span>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,72 @@
{% extends phone_or_desktop %}
{% load my_filters %}
{% block title %}RustDesk WebUI{% endblock %}
{% block legend_name %}{{ "File Transfer Log" | translate }}{% endblock %}
{% load static %}
{% block content %}
<script src="{% static 'js/sorttable.js' %}"></script>
<div style="padding: 20px; background-color: #F2F2F2;">
<div class="layui-row layui-col-space15">
<div class="layui-col-md15">
<input type="text" id="globalSearchInput" onkeyup="globalSearch()" placeholder="Search all fields..." style="width: 200px; height: 20px; margin-bottom: 10px;">
<div class="layui-card">
<div class="layui-card-header">{{ "File Transfer Log" }}:【{{u.username}}】</div>
<div class="layui-card-body">
<table class="layui-table sortable">
<thead>
<tr>
<th>File</th>
<th>Remote ID</th>
<th>Remote Alias</th>
<th>User ID</th>
<th>User Alias</th>
<th>User IP</th>
<th>Filesize</th>
<th>Sent/Received</th>
<th>Logged At</th>
</tr>
</thead>
<tbody>
{% for one in page_obj %}
<tr>
<td>{{one.file}}</td>
<td>{{one.remote_id}} </td>
<td>{{one.remote_alias}}</td>
<td>{{one.user_id}}</td>
<td>{{one.user_alias}}</td>
<td>{{one.user_ip}}</td>
<td>{{one.filesize}}</td>
{% if one.direction == 0 %}
<td>User Received File</td>
{% else %}
<td>User Sent File</td>
{% endif %}
<td>{{one.logged_at}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-md4 layui-col-md-offset4">
<span class="step-links">
{% if page_obj.has_previous %}
<button class="layui-btn" ><a href="?page=1">&laquo; {{ "首页" | translate }}</a></button>
<button class="layui-btn" ><a href="?page={{ page_obj.previous_page_number }}">{{ "上一页" | translate }}</a></button>
{% endif %}
{% if page_obj.paginator.num_pages > 1 %}
<span class="current">
{{ "页码" | translate }} {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
</span>
{% endif %}
{% if page_obj.has_next %}
<button class="layui-btn" > <a href="?page={{ page_obj.next_page_number }}">{{ "下一页" | translate }}</a></button>
<button class="layui-btn" ><a href="?page={{ page_obj.paginator.num_pages }}">{{ "尾页" | translate }} &raquo;</a></button>
{% endif %}
</span>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends phone_or_desktop %}
{% load my_filters %}
{% block title %}RustDesk WebUI{% endblock %}
{% block legend_name %}{{ "Server Information" | translate }}{% endblock %}
{% block content %}
<br>Hostname:
{{ hostname }}
<br>CPU Usage:
{{ cpu_usage }}
<br>Memory Usage:
{{ memory_usage }}
<br>Disk Usage:
{{ disk_usage }}
{% endblock %}

View File

@@ -0,0 +1,161 @@
{% extends phone_or_desktop %}
{% load my_filters %}
{% block title %}RustDesk WebUI{% endblock %}
{% block legend_name %}{{ "Devices" | translate }}{% endblock %}
{% load static %}
{% block content %}
<script src="{% static 'js/sorttable.js' %}"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<div style="padding: 20px; background-color: #F2F2F2;">
<div class="layui-row layui-col-space15">
{% if not show_all %}
<div class="layui-col-md15">
<input type="text" id="globalSearchInput" onkeyup="globalSearch()" placeholder="Search all fields..." style="width: 200px; height: 20px; margin-bottom: 10px;">
<div class="layui-card">
<div class="layui-card-header">{{ online_count_single }}/{{page_obj.paginator.count }}: {{ "User Devices Online" | translate }} - 【{{ "用户名" | translate }}:{{u.username}}】
<a href="/api/add_peer"><i class='fa fa-plus'></i> Add Client</a>
</div>
<div class="layui-card-body">
<table class="layui-table sortable">
<thead>
<tr>
<th>Edit / Delete</th>
<th>{{ "客户端ID" | translate }}</th>
<th>{{ "状态" | translate }}</th>
<th>{{ "别名" | translate }}</th>
<th>{{ "版本" | translate }}</th>
<th>{{ "连接密码" | translate }}</th>
<th>{{ "系统用户名" | translate }}</th>
<th>{{ "计算机名" | translate }}</th>
<th>{{ "平台" | translate }}</th>
<th>{{ "系统" | translate }}</th>
<th>{{ "CPU" | translate }}</th>
<th>{{ "内存" | translate }}</th>
<th>{{ "注册时间" | translate }}</th>
<th>{{ "更新时间" | translate }}</th>
<th>{{ "IP Address" | translate }}</th>
</tr>
</thead>
<tbody>
{% for one in page_obj %}
<tr>
<td><a href="/api/edit_peer?rid={{one.rid}}"><i class='fa fa-edit'></i></a> / <a href="/api/delete_peer?rid={{one.rid}}"><i class='fa fa-trash'></i></a></td>
<td><a href=rustdesk://{{one.rid}}>{{one.rid}}</a> </td>
<td>{{one.status}} </td>
<td>{{one.alias}}</td>
<td>{{one.version}}</td>
<td>{{one.has_rhash}}</td>
<td>{{one.username}}</td>
<td>{{one.hostname}}</td>
<td>{{one.platform}}</td>
<td>{{one.os}}</td>
<td>{{one.cpu}}</td>
<td>{{one.memory}}</td>
<td>{{one.create_time}}</td>
<td>{{one.update_time}}</td>
<td>{{one.ip}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-md4 layui-col-md-offset4">
<span class="step-links">
{% if page_obj.has_previous %}
<button class="layui-btn" ><a href="?page=1">&laquo; {{ "首页" | translate }}</a></button>
<button class="layui-btn" ><a href="?page={{ page_obj.previous_page_number }}">{{ "上一页" | translate }}</a></button>
{% endif %}
{% if page_obj.paginator.num_pages > 1 %}
<span class="current">
{{ "页码" | translate }} {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
</span>
{% endif %}
{% if page_obj.has_next %}
<button class="layui-btn" > <a href="?page={{ page_obj.next_page_number }}">{{ "下一页" | translate }}</a></button>
<button class="layui-btn" ><a href="?page={{ page_obj.paginator.num_pages }}">{{ "尾页" | translate }} &raquo;</a></button>
{% endif %}
</span>
</div>
{% endif %}
{% if u.is_admin and show_all %}
<div class="layui-col-md15">
<input type="text" id="globalSearchInput" onkeyup="globalSearch()" placeholder="Search all fields..." style="width: 200px; height: 20px; margin-bottom: 10px;">
<div class="layui-card">
<div class="layui-card-header">{{ online_count_all }}/{{page_obj.paginator.count }}: {{ "All Devices" | translate }} &raquo;
<div class="layui-btn" ><a href="/api/down_peers">{{ "导出xlsx" | translate }}</a></div>
</div>
<div class="layui-card-body">
<table class="layui-table sortable">
<thead>
<tr>
<th>{{ "客户端ID" | translate }}</th>
<th>{{ "所属用户" | translate }}</th>
<th>{{ "版本" | translate }}</th>
<th>{{ "系统用户名" | translate }}</th>
<th>{{ "计算机名" | translate }}</th>
<th>{{ "系统" | translate }}</th>
<th>{{ "CPU" | translate }}</th>
<th>{{ "内存" | translate }}</th>
<th>{{ "注册日期" | translate }}</th>
<th>{{ "更新时间" | translate }}</th>
<th>{{ "状态" | translate }}</th>
<th>{{ "IP Address" | translate }}</th>
</tr>
</thead>
<tbody>
{% for one in page_obj %}
<tr>
<td><a href=rustdesk://{{one.rid}}>{{one.rid}}</a> </td>
{% if one.rust_user|length > 0 %}
<td>{{one.rust_user}} </td>
{% else %}
<td><a href="/api/assign_peer?rid={{one.rid}}"><i class='fa fa-plus'></i> Assign Peer</a></td>
{% endif %}
<td>{{one.version}} </td>
<td>{{one.username}} </td>
<td>{{one.hostname}} </td>
<td>{{one.os}} </td>
<td>{{one.cpu}} </td>
<td>{{one.memory}} </td>
<td>{{one.create_time}} </td>
<td>{{one.update_time}} </td>
<td>{{one.status}} </td>
<td>{{one.ip}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="layui-col-md4 layui-col-md-offset4">
<span class="step-links">
{% if page_obj.has_previous %}
<button class="layui-btn" ><a href="?show_type=admin&page=1">&laquo; {{ "首页" | translate }}</a></button>
<button class="layui-btn" ><a href="?show_type=admin&page={{ page_obj.previous_page_number }}">{{ "上一页" | translate }}</a></button>
{% endif %}
{% if page_obj.paginator.num_pages > 1 %}
<span class="current">
{{ "页码" | translate }} {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
</span>
{% endif %}
{% if page_obj.has_next %}
<button class="layui-btn" > <a href="?show_type=admin&page={{ page_obj.next_page_number }}">{{ "下一页" | translate }}</a></button>
<button class="layui-btn" ><a href="?show_type=admin&page={{ page_obj.paginator.num_pages }}">{{ "尾页" | translate }} &raquo;</a></button>
{% endif %}
</span>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends phone_or_desktop %}
{% load my_filters %}
{% block title %}RustDesk WebUI{% endblock %}
{% block legend_name %}{{ "Client Generator" | translate }}{% endblock %}
{% block content %}
Please wait...This can take 20-30 minutes (or longer if there are other users).<br><br>
Status: {{status}}
<script>
setTimeout(function() {
window.location.replace('/api/check_for_file?filename={{filename}}&uuid={{uuid}}&platform={{platform}}');
}, 5000); // 5000 milliseconds = 5 seconds
</script>
{% endblock %}

View File

View File

@@ -0,0 +1,8 @@
from django import template
from django.utils.translation import gettext as _
register = template.Library()
@register.filter
def translate(text):
return _(text)

3
api/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

42
api/urls.py Normal file
View File

@@ -0,0 +1,42 @@
import django
if django.__version__.split('.')[0]>='4':
from django.urls import re_path as url
else:
from django.conf.urls import url, include
from api import views
urlpatterns = [
url(r'^login',views.login),
url(r'^logout',views.logout),
url(r'^ab$',views.ab),
url(r'^ab\/get',views.ab_get), # 兼容 x86-sciter 版客户端
url(r'^users',views.users),
url(r'^peers',views.peers),
url(r'^currentUser',views.currentUser),
url(r'^sysinfo',views.sysinfo),
url(r'^heartbeat',views.heartbeat),
#url(r'^register',views.register),
url(r'^user_action',views.user_action), # 前端
url(r'^work',views.work), # 前端
url(r'^down_peers$',views.down_peers), # 前端
url(r'^share',views.share), # 前端
url(r'^conn_log',views.conn_log),
url(r'^file_log',views.file_log),
url(r'^audit',views.audit),
url(r'^sys_info',views.sys_info),
url(r'^clients',views.clients),
url(r'^download',views.download),
url(r'^generator',views.generator_view),
url(r'^check_for_file',views.check_for_file),
url(r'^download_client',views.download_client),
url(r'^creategh',views.create_github_run),
url(r'^updategh',views.update_github_run),
url(r'^save_custom_client',views.save_custom_client),
url(r'^delete_file',views.delete_file),
url(r'^get_png',views.get_png),
url(r'^add_peer',views.add_peer),
url(r'^delete_peer',views.delete_peer),
url(r'^edit_peer',views.edit_peer),
url(r'^assign_peer',views.assign_peer),
]

36
api/util.py Normal file
View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
"""
Created on Thu Nov 19 15:51:21 2020
@author: lenovo
"""
import platform
import logging
from .models_user import UserProfile
logger = logging.getLogger(__name__)
from django.conf import settings as _settings
def settings(request):
"""
TEMPLATE_CONTEXT_PROCESSORS
"""
context = { 'settings': _settings }
try:
username = request.user
u = UserProfile.objects.get(username=username)
context['test'] = 'This is a test variable'
context['u'] = u
#context['user'] = u
context['username'] = username
context['is_admin'] = u.is_admin
context['is_active'] = u.is_active
context['domain'] = _settings.ID_SERVER
context['is_windows'] = True if platform.system() == 'Windows' else False
logger.info("set system status variable")
except Exception as e:
logger.error("settings:{}".format( e))
return context

26
api/views.py Normal file
View File

@@ -0,0 +1,26 @@
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.contrib.auth.hashers import make_password
from django.db.models import Q
from django.contrib.auth.decorators import login_required
from django.contrib import auth
from django.forms.models import model_to_dict
from itertools import chain
from django.db.models.fields import DateTimeField, DateField, CharField, TextField
from django.db.models import Model
from django.http import JsonResponse
import json
import time
import datetime
import hashlib
from api.models import RustDeskToken, UserProfile, RustDeskTag, RustDeskPeer, RustDesDevice
import copy
from .views_front import *
from .views_api import *
from .views_generator import *
from .front_locale import *

329
api/views_api.py Normal file
View File

@@ -0,0 +1,329 @@
# cython:language_level=3
from django.http import JsonResponse
import json
import time
import datetime
import hashlib
import math
from django.contrib import auth
from django.forms.models import model_to_dict
from api.models import RustDeskToken, UserProfile, RustDeskTag, RustDeskPeer, RustDesDevice, ConnLog, FileLog
from django.db.models import Q
import copy
from .views_front import *
from django.utils.translation import gettext as _
def login(request):
result = {}
if request.method == 'GET':
result['error'] = _('请求方式错误请使用POST方式。')
return JsonResponse(result)
data = json.loads(request.body.decode())
username = data.get('username', '')
password = data.get('password', '')
rid = data.get('id', '')
uuid = data.get('uuid', '')
autoLogin = data.get('autoLogin', True)
rtype = data.get('type', '')
deviceInfo = data.get('deviceInfo', '')
user = auth.authenticate(username=username,password=password)
if not user:
result['error'] = _('帐号或密码错误请重试多次重试后将被锁定IP')
return JsonResponse(result)
user.rid = rid
user.uuid = uuid
user.autoLogin = autoLogin
user.rtype = rtype
user.deviceInfo = json.dumps(deviceInfo)
user.save()
token = RustDeskToken.objects.filter(Q(uid=user.id) & Q(username=user.username) & Q(rid=user.rid)).first()
# Check whether
if token:
now_t = datetime.datetime.now()
nums = (now_t - token.create_time).seconds if now_t > token.create_time else 0
if nums >= EFFECTIVE_SECONDS:
token.delete()
token = None
if not token:
# Get and save token
token = RustDeskToken(
username=user.username,
uid=user.id,
uuid=user.uuid,
rid=user.rid,
access_token=getStrMd5(str(time.time())+salt)
)
token.save()
result['access_token'] = token.access_token
result['type'] = 'access_token'
result['user'] = {'name':user.username}
return JsonResponse(result)
def logout(request):
if request.method == 'GET':
result = {'error':_('请求方式错误!')}
return JsonResponse(result)
data = json.loads(request.body.decode())
rid = data.get('id', '')
uuid = data.get('uuid', '')
user = UserProfile.objects.filter(Q(rid=rid) & Q(uuid=uuid)).first()
if not user:
result = {'error':_('异常请求!')}
return JsonResponse(result)
token = RustDeskToken.objects.filter(Q(uid=user.id) & Q(rid=user.rid)).first()
if token:
token.delete()
result = {'code':1}
return JsonResponse(result)
def currentUser(request):
result = {}
if request.method == 'GET':
result['error'] = _('错误的提交方式!')
return JsonResponse(result)
postdata = json.loads(request.body)
rid = postdata.get('id', '')
uuid = postdata.get('uuid', '')
access_token = request.META.get('HTTP_AUTHORIZATION', '')
access_token = access_token.split('Bearer ')[-1]
token = RustDeskToken.objects.filter(Q(access_token=access_token) ).first()
user = None
if token:
user = UserProfile.objects.filter(Q(id=token.uid)).first()
if user:
if token:
result['access_token'] = token.access_token
result['type'] = 'access_token'
result['name'] = user.username
return JsonResponse(result)
def ab(request):
'''
'''
access_token = request.META.get('HTTP_AUTHORIZATION', '')
access_token = access_token.split('Bearer ')[-1]
token = RustDeskToken.objects.filter(Q(access_token=access_token) ).first()
if not token:
result = {'error':_('拉取列表错误!')}
return JsonResponse(result)
if request.method == 'GET':
result = {}
uid = token.uid
tags = RustDeskTag.objects.filter(Q(uid=uid) )
tag_names = []
tag_colors = {}
if tags:
tag_names = [str(x.tag_name) for x in tags]
tag_colors = {str(x.tag_name):int(x.tag_color) for x in tags if x.tag_color!=''}
peers_result = []
peers = RustDeskPeer.objects.filter(Q(uid=uid) )
if peers:
for peer in peers:
tmp = {
'id':peer.rid,
'username':peer.username,
'hostname':peer.hostname,
'alias':peer.alias,
'platform':peer.platform,
'tags':peer.tags.split(','),
'hash':peer.rhash,
}
peers_result.append(tmp)
result['updated_at'] = datetime.datetime.now()
result['data'] = {
'tags':tag_names,
'peers':peers_result,
'tag_colors':json.dumps(tag_colors)
}
result['data'] = json.dumps(result['data'])
return JsonResponse(result)
else:
postdata = json.loads(request.body.decode())
data = postdata.get('data', '')
data = {} if data=='' else json.loads(data)
tagnames = data.get('tags', [])
tag_colors = data.get('tag_colors', '')
tag_colors = {} if tag_colors=='' else json.loads(tag_colors)
peers = data.get('peers', [])
if tagnames:
# Delete the old tag
RustDeskTag.objects.filter(uid=token.uid).delete()
# Increase
newlist = []
for name in tagnames:
tag = RustDeskTag(
uid=token.uid,
tag_name=name,
tag_color=tag_colors.get(name, '')
)
newlist.append(tag)
RustDeskTag.objects.bulk_create(newlist)
if peers:
RustDeskPeer.objects.filter(uid=token.uid).delete()
newlist = []
for one in peers:
peer = RustDeskPeer(
uid=token.uid,
rid=one['id'],
username=one['username'],
hostname=one['hostname'],
alias=one['alias'],
platform=one['platform'],
tags=','.join(one['tags']),
rhash=one['hash'],
)
newlist.append(peer)
RustDeskPeer.objects.bulk_create(newlist)
result = {
'code':102,
'data':_('更新地址簿有误')
}
return JsonResponse(result)
def ab_get(request):
# 兼容 x86-sciter 版客户端,此版客户端通过访问 "POST /api/ab/get" 来获取地址簿
request.method = 'GET'
return ab(request)
def sysinfo(request):
# 客户端注册服务后,才会发送设备信息
result = {}
if request.method == 'GET':
result['error'] = _('错误的提交方式!')
return JsonResponse(result)
client_ip = get_client_ip(request)
postdata = json.loads(request.body)
device = RustDesDevice.objects.filter(Q(rid=postdata['id']) & Q(uuid=postdata['uuid']) ).first()
if not device:
device = RustDesDevice(
rid=postdata['id'],
cpu=postdata['cpu'],
hostname=postdata['hostname'],
memory=postdata['memory'],
os=postdata['os'],
username=postdata.get('username', '-'),
uuid=postdata['uuid'],
version=postdata['version'],
ip=client_ip,
)
device.save()
else:
postdata2 = copy.copy(postdata)
postdata2['rid'] = postdata2['id']
postdata2.pop('id')
postdata2['ip'] = client_ip
RustDesDevice.objects.filter(Q(rid=postdata['id']) & Q(uuid=postdata['uuid']) ).update(**postdata2)
result['data'] = 'ok'
return JsonResponse(result)
def heartbeat(request):
postdata = json.loads(request.body)
device = RustDesDevice.objects.filter(Q(rid=postdata['id']) & Q(uuid=postdata['uuid']) ).first()
if device:
device.save()
# token保活
create_time = datetime.datetime.now() + datetime.timedelta(seconds=EFFECTIVE_SECONDS)
RustDeskToken.objects.filter(Q(rid=postdata['id']) & Q(uuid=postdata['uuid']) ).update(create_time=create_time)
result = {}
result['data'] = _('在线')
return JsonResponse(result)
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def convert_filesize(size_bytes):
if size_bytes == 0:
return "0B"
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return "%s %s" % (s, size_name[i])
def audit(request):
postdata = json.loads(request.body)
#print(postdata)
audit_type = postdata['action'] if 'action' in postdata else ''
if audit_type == 'new':
new_conn_log = ConnLog(
action=postdata['action'] if 'action' in postdata else '',
conn_id=postdata['conn_id'] if 'conn_id' in postdata else 0,
from_ip=postdata['ip'] if 'ip' in postdata else '',
from_id='',
rid=postdata['id'] if 'id' in postdata else '',
conn_start=datetime.datetime.now(),
session_id=postdata['session_id'] if 'session_id' in postdata else 0,
uuid=postdata['uuid'] if 'uuid' in postdata else '',
)
new_conn_log.save()
elif audit_type =="close":
ConnLog.objects.filter(Q(conn_id=postdata['conn_id'])).update(conn_end=datetime.datetime.now())
elif 'is_file' in postdata:
print(postdata)
files = json.loads(postdata['info'])['files']
filesize = convert_filesize(int(files[0][1]))
new_file_log = FileLog(
file=postdata['path'],
user_id=postdata['peer_id'],
user_ip=json.loads(postdata['info'])['ip'],
remote_id=postdata['id'],
filesize=filesize,
direction=postdata['type'],
logged_at=datetime.datetime.now(),
)
new_file_log.save()
else:
try:
peer = postdata['peer']
ConnLog.objects.filter(Q(conn_id=postdata['conn_id'])).update(session_id=postdata['session_id'])
ConnLog.objects.filter(Q(conn_id=postdata['conn_id'])).update(from_id=peer[0])
except:
print(postdata)
result = {
'code':1,
'data':'ok'
}
return JsonResponse(result)
def users(request):
result = {
'code':1,
'data':_('好的')
}
return JsonResponse(result)
def peers(request):
result = {
'code':1,
'data':'ok'
}
return JsonResponse(result)

719
api/views_front.py Normal file
View File

@@ -0,0 +1,719 @@
# cython:language_level=3
from pathlib import Path
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.contrib.auth.hashers import make_password
from django.http import JsonResponse
from django.db.models import Q
from django.contrib.auth.decorators import login_required
from django.contrib import auth
from api.models import RustDeskPeer, RustDesDevice, UserProfile, ShareLink, ConnLog, FileLog
from django.forms.models import model_to_dict
from django.core.paginator import Paginator
from django.http import HttpResponse
from django.conf import settings
from itertools import chain
from django.db.models.fields import DateTimeField, DateField, CharField, TextField
import datetime
from django.db.models import Model
import json
import time
import hashlib
import sys
from dateutil import tz
import platform
import psutil
import os
from io import BytesIO
import xlwt
from django.utils.translation import gettext as _
from .forms import AddPeerForm, EditPeerForm, AssignPeerForm
BASE_DIR = Path(__file__).resolve().parent.parent
salt = 'xiaomo'
EFFECTIVE_SECONDS = 7200
def getStrMd5(s):
if not isinstance(s, (str,)):
s = str(s)
myHash = hashlib.md5()
myHash.update(s.encode())
return myHash.hexdigest()
def model_to_dict2(instance, fields=None, exclude=None, replace=None, default=None):
"""
:params instance: Model object, not the QuerySet data set
:params fields: Specify the field data to be displayed,('Field 1','Field 2')
:params exclude: Specify the field data that is eliminated,('Field 1','Field 2')
:params replace: Modify the field name to the required name,{'Database field name':'Front -end display name'}
:params default: Added no existing field data,{'Field':'data'}
"""
# 对传递进来的模型对象校验
if not isinstance(instance, Model):
raise Exception(_('model_to_dict接收的参数必须是模型对象'))
# 对替换数据库字段名字校验
if replace and type(replace) == dict:
for replace_field in replace.values():
if hasattr(instance, replace_field):
raise Exception(_(f'model_to_dict,要替换成{replace_field}字段已经存在了'))
# 对要新增的默认值进行校验
if default and type(default) == dict:
for default_key in default.keys():
if hasattr(instance, default_key):
raise Exception(_(f'model_to_dict,要新增默认值,但字段{default_key}已经存在了'))
opts = instance._meta
data = {}
for f in chain(opts.concrete_fields, opts.private_fields, opts.many_to_many):
# 源码下:这块代码会将时间字段剔除掉,我加上一层判断,让其不再剔除时间字段
if not getattr(f, 'editable', False):
if type(f) == DateField or type(f) == DateTimeField:
pass
else:
continue
# 如果fields参数传递了要进行判断
if fields is not None and f.name not in fields:
continue
# 如果exclude 传递了,要进行判断
if exclude and f.name in exclude:
continue
key = f.name
# 获取字段对应的数据
if type(f) == DateTimeField:
# 字段类型是DateTimeFiled 使用自己的方式操作
value = getattr(instance, key)
value = datetime.datetime.strftime(value, '%Y-%m-%d %H:%M')
elif type(f) == DateField:
# 字段类型是DateFiled 使用自己的方式操作
value = getattr(instance, key)
value = datetime.datetime.strftime(value, '%Y-%m-%d')
elif type(f) == CharField or type(f) == TextField:
# 字符串数据是否可以进行序列化转成python结构数据
value = getattr(instance, key)
try:
value = json.loads(value)
except Exception as _:
value = value
else:#其他类型的字段
# value = getattr(instance, key)
key = f.name
value = f.value_from_object(instance)
# data[f.name] = f.value_from_object(instance)
# 1、替换字段名字
if replace and key in replace.keys():
key = replace.get(key)
data[key] = value
#2、新增默认的字段数据
if default:
data.update(default)
return data
def index(request):
print('sdf',sys.argv)
if request.user and request.user.username!='AnonymousUser':
return HttpResponseRedirect('/api/work')
return HttpResponseRedirect('/api/user_action?action=login')
def user_action(request):
action = request.GET.get('action', '')
if action == 'login':
return user_login(request)
elif action == 'register':
return user_register(request)
elif action == 'logout':
return user_logout(request)
else:
return
def user_login(request):
if request.method == 'GET':
return render(request, 'login.html')
username = request.POST.get('account', '')
password = request.POST.get('password', '')
if not username or not password:
return JsonResponse({'code':0, 'msg':_('出了点问题,未获取用户名或密码。')})
user = auth.authenticate(username=username,password=password)
if user:
auth.login(request, user)
return JsonResponse({'code':1, 'url':'/api/work'})
else:
return JsonResponse({'code':0, 'msg':_('帐号或密码错误!')})
def user_register(request):
info = ''
if request.method == 'GET':
return render(request, 'reg.html')
ALLOW_REGISTRATION = settings.ALLOW_REGISTRATION
result = {
'code':0,
'msg':''
}
if not ALLOW_REGISTRATION:
result['msg'] = _('当前未开放注册,请联系管理员!')
return JsonResponse(result)
username = request.POST.get('user', '')
password1 = request.POST.get('pwd', '')
if len(username) <= 3:
info = _('用户名不得小于3位')
result['msg'] = info
return JsonResponse(result)
if len(password1)<8 or len(password1)>20:
info = _('密码长度不符合要求, 应在8~20位。')
result['msg'] = info
return JsonResponse(result)
user = UserProfile.objects.filter(Q(username=username)).first()
if user:
info = _('用户名已存在。')
result['msg'] = info
return JsonResponse(result)
user = UserProfile(
username=username,
password=make_password(password1),
is_admin = True if UserProfile.objects.count()==0 else False,
is_superuser = True if UserProfile.objects.count()==0 else False,
is_active = True
)
user.save()
result['msg'] = info
result['code'] = 1
return JsonResponse(result)
@login_required(login_url='/api/user_action?action=login')
def user_logout(request):
info = ''
auth.logout(request)
return HttpResponseRedirect('/api/user_action?action=login')
def get_single_info(uid):
online_count = 0
peers = RustDeskPeer.objects.filter(Q(uid=uid))
rids = [x.rid for x in peers]
peers = {x.rid:model_to_dict(x) for x in peers}
#print(peers)
devices = RustDesDevice.objects.filter(rid__in=rids)
devices = {x.rid:x for x in devices}
for rid in peers.keys():
peers[rid]['has_rhash'] = _('yes') if len(peers[rid]['rhash'])>1 else _('no')
peers[rid]['status'] = _('X')
now = datetime.datetime.now()
for rid, device in devices.items():
peers[rid]['create_time'] = device.create_time.strftime('%Y-%m-%d')
peers[rid]['update_time'] = device.update_time.strftime('%Y-%m-%d %H:%M')
peers[rid]['version'] = device.version
peers[rid]['memory'] = device.memory
peers[rid]['cpu'] = device.cpu
peers[rid]['os'] = device.os
peers[rid]['ip'] = device.ip
if (now-device.update_time).seconds <=120:
peers[rid]['status'] = _('Online')
online_count += 1
else:
peers[rid]['status'] = _('X')
sorted_peers = sorted(peers.items(), key=custom_sort, reverse=True)
new_ordered_dict = {}
for key, peer in sorted_peers:
new_ordered_dict[key] = peer
#return ([v for k,v in peers.items()], online_count)
return ([v for k,v in new_ordered_dict.items()], online_count)
def get_all_info():
online_count = 0
devices = RustDesDevice.objects.all()
peers = RustDeskPeer.objects.all()
devices = {x.rid:model_to_dict2(x) for x in devices}
now = datetime.datetime.now()
for peer in peers:
user = UserProfile.objects.filter(Q(id=peer.uid)).first()
device = devices.get(peer.rid, None)
if device:
devices[peer.rid]['rust_user'] = user.username
for k, v in devices.items():
if (now-datetime.datetime.strptime(v['update_time'], '%Y-%m-%d %H:%M')).seconds <=120:
devices[k]['status'] = _('Online')
online_count += 1
else:
devices[k]['status'] = _('X')
sorted_devices = sorted(devices.items(), key=custom_sort, reverse=True)
new_ordered_dict = {}
for key, device in sorted_devices:
new_ordered_dict[key] = device
return ([v for k,v in new_ordered_dict.items()], online_count)
def custom_sort(item):
status = item[1]['status']
if status == 'Online':
return 1
else:
return 0
def get_conn_log():
logs = ConnLog.objects.all()
logs = {x.id:model_to_dict(x) for x in logs}
for k, v in logs.items():
try:
peer = RustDeskPeer.objects.get(rid=v['rid'])
logs[k]['alias'] = peer.alias
except:
logs[k]['alias'] = 'UNKNOWN'
try:
peer = RustDeskPeer.objects.get(rid=v['from_id'])
logs[k]['from_alias'] = peer.alias
except:
logs[k]['from_alias'] = 'UNKNOWN'
#from_zone = tz.tzutc()
#to_zone = tz.tzlocal()
#utc = logs[k]['logged_at']
#utc = utc.replace(tzinfo=from_zone)
#logs[k]['logged_at'] = utc.astimezone(to_zone)
try:
duration = round((logs[k]['conn_end'] - logs[k]['conn_start']).total_seconds())
m, s = divmod(duration, 60)
h, m = divmod(m, 60)
#d, h = divmod(h, 24)
logs[k]['duration'] = f'{h:02d}:{m:02d}:{s:02d}'
except:
logs[k]['duration'] = -1
sorted_logs = sorted(logs.items(), key=lambda x: x[1]['conn_start'], reverse=True)
new_ordered_dict = {}
for key, alog in sorted_logs:
new_ordered_dict[key] = alog
return [v for k, v in new_ordered_dict.items()]
def get_file_log():
logs = FileLog.objects.all()
logs = {x.id:model_to_dict(x) for x in logs}
for k, v in logs.items():
try:
peer_remote = RustDeskPeer.objects.get(rid=v['remote_id'])
logs[k]['remote_alias'] = peer_remote.alias
except:
logs[k]['remote_alias'] = 'UNKNOWN'
try:
peer_user = RustDeskPeer.objects.get(rid=v['user_id'])
logs[k]['user_alias'] = peer_user.alias
except:
logs[k]['user_alias'] = 'UNKNOWN'
sorted_logs = sorted(logs.items(), key=lambda x: x[1]['logged_at'], reverse=True)
new_ordered_dict = {}
for key, alog in sorted_logs:
new_ordered_dict[key] = alog
return [v for k, v in new_ordered_dict.items()]
@login_required(login_url='/api/user_action?action=login')
def sys_info(request):
hostname = platform.node()
cpu_usage = psutil.cpu_percent()
memory_usage = psutil.virtual_memory().percent
disk_usage = psutil.disk_usage('/').percent
print(cpu_usage, memory_usage, disk_usage)
return render(request, 'show_sys_info.html', {'hostname':hostname, 'cpu_usage':cpu_usage, 'memory_usage':memory_usage, 'disk_usage':disk_usage, 'phone_or_desktop': is_mobile(request)})
@login_required(login_url='/api/user_action?action=login')
def clients(request):
basedir = os.path.join('clients')
androidaarch64 = os.path.join(basedir,'android','aarch64')
androidarmv7 = os.path.join(basedir,'android','armv7')
linuxaarch64 = os.path.join(basedir,'linux','aarch64')
linuxx86_64 = os.path.join(basedir,'linux','x86_64')
mocos = os.path.join(basedir,'macOS')
sciter = os.path.join(basedir,'sciter')
custom = os.path.join(basedir,'custom')
client_files = {}
client_custom_files = {}
if os.path.exists(basedir):
for file in os.listdir(basedir):
if (file.endswith(".exe") or file.endswith(".msi")):
filepath = os.path.join(basedir,file)
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
client_files[file] = {
'file': file,
'modified': modified,
'path': basedir
}
if os.path.exists(androidaarch64):
for file in os.listdir(androidaarch64):
if file.endswith(".apk"):
filepath = os.path.join(androidaarch64,file)
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
client_files[file] = {
'file': file,
'modified': modified,
'path': androidaarch64
}
if os.path.exists(androidarmv7):
for file in os.listdir(androidarmv7):
if file.endswith(".apk"):
filepath = os.path.join(androidarmv7,file)
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
client_files[file] = {
'file': file,
'modified': modified,
'path': androidarmv7
}
if os.path.exists(linuxaarch64):
for file in os.listdir(linuxaarch64):
if (file.endswith(".rpm") or file.endswith(".deb")):
filepath = os.path.join(linuxaarch64,file)
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
client_files[file] = {
'file': file,
'modified': modified,
'path': linuxaarch64
}
if os.path.exists(linuxx86_64):
for file in os.listdir(linuxx86_64):
if (file.endswith(".rpm") or file.endswith(".deb")):
filepath = os.path.join(linuxx86_64,file)
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
client_files[file] = {
'file': file,
'modified': modified,
'path': linuxx86_64
}
if os.path.exists(mocos):
for file in os.listdir(mocos):
if file.endswith(".dmg"):
filepath = os.path.join(mocos,file)
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
client_files[file] = {
'file': file,
'modified': modified,
'path': mocos
}
if os.path.exists(sciter):
for file in os.listdir(sciter):
if file.endswith(".exe"):
filepath = os.path.join(sciter,file)
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
client_files[file] = {
'file': file,
'modified': modified,
'path': sciter
}
if os.path.exists(custom):
for file in os.listdir(custom):
#if file.endswith(".exe"):
filepath = os.path.join(custom,file)
modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
client_custom_files[file] = {
'file': file,
'modified': modified,
'path': custom
}
return render(request, 'clients.html', {'client_files': client_files, 'client_custom_files': client_custom_files, 'phone_or_desktop': is_mobile(request)})
def download_file(request, filename, path):
file_path = os.path.join(str(BASE_DIR),path,filename)
with open(file_path, 'rb') as file:
response = HttpResponse(file, headers={
'Content-Type': 'application/x-binary',
'Content-Disposition': f'attachment; filename="{filename}"'
})
return response
@login_required(login_url='/api/user_action?action=login')
def download(request):
filename = request.GET['filename']
path = request.GET['path']
return download_file(request, filename, path)
@login_required(login_url='/api/user_cation?action=login')
def delete_file(request):
filename = request.GET['filename']
path = request.GET['path']
file_path = os.path.join(str(BASE_DIR),path,filename)
if os.path.isfile(file_path):
os.remove(file_path)
return HttpResponseRedirect('/api/clients')
@login_required(login_url='/api/user_action?action=login')
def add_peer(request):
if request.method == 'POST':
form = AddPeerForm(request.POST)
if form.is_valid():
rid = form.cleaned_data['clientID']
uid = request.user.id
username = form.cleaned_data['username']
hostname = form.cleaned_data['hostname']
plat = form.cleaned_data['platform']
alias = form.cleaned_data['alias']
tags = form.cleaned_data['tags']
ip = form.cleaned_data['ip']
peer = RustDeskPeer(
uid = uid,
rid = rid,
username = username,
hostname = hostname,
platform = plat,
alias = alias,
tags = tags,
ip = ip
)
peer.save()
return HttpResponseRedirect('/api/work')
else:
rid = request.GET.get('rid','')
form = AddPeerForm()
return render(request, 'add_peer.html', {'form': form, 'rid': rid, 'phone_or_desktop': is_mobile(request)})
@login_required(login_url='/api/user_action?action=login')
def edit_peer(request):
if request.method == 'POST':
form = EditPeerForm(request.POST)
if form.is_valid():
rid = form.cleaned_data['clientID']
uid = request.user.id
username = form.cleaned_data['username']
hostname = form.cleaned_data['hostname']
plat = form.cleaned_data['platform']
alias = form.cleaned_data['alias']
tags = form.cleaned_data['tags']
updated_peer = RustDeskPeer.objects.get(rid=rid,uid=uid)
updated_peer.username=username
updated_peer.hostname=hostname
updated_peer.platform=plat
updated_peer.alias=alias
updated_peer.tags=tags
updated_peer.save()
return HttpResponseRedirect('/api/work')
else:
print(form.errors)
else:
rid = request.GET.get('rid','')
peer = RustDeskPeer.objects.get(rid=rid)
initial_data = {
'clientID': rid,
'alias': peer.alias,
'tags': peer.tags,
'username': peer.username,
'hostname': peer.hostname,
'platform': peer.platform,
'ip': peer.ip
}
form = EditPeerForm(initial=initial_data)
return render(request, 'edit_peer.html', {'form': form, 'peer': peer, 'phone_or_desktop': is_mobile(request)})
@login_required(login_url='/api/user_action?action=login')
def assign_peer(request):
if request.method == 'POST':
form = AssignPeerForm(request.POST)
if form.is_valid():
rid = form.cleaned_data['clientID']
uid = form.cleaned_data['uid']
username = form.cleaned_data['username']
hostname = form.cleaned_data['hostname']
plat = form.cleaned_data['platform']
alias = form.cleaned_data['alias']
tags = form.cleaned_data['tags']
ip = form.cleaned_data['ip']
peer = RustDeskPeer(
uid = uid.id,
rid = rid,
username = username,
hostname = hostname,
platform = plat,
alias = alias,
tags = tags,
ip = ip
)
peer.save()
return HttpResponseRedirect('/api/work')
else:
print(form.errors)
else:
rid = request.GET.get('rid')
form = AssignPeerForm()
#get list of users from the database
return render(request, 'assign_peer.html', {'form':form, 'rid': rid})
@login_required(login_url='/api/user_action?action=login')
def delete_peer(request):
rid = request.GET.get('rid')
peer = RustDeskPeer.objects.filter(Q(uid=request.user.id) & Q(rid=rid))
peer.delete()
return HttpResponseRedirect('/api/work')
@login_required(login_url='/api/user_action?action=login')
def conn_log(request):
paginator = Paginator(get_conn_log(), 20)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'show_conn_log.html', {'page_obj':page_obj, 'phone_or_desktop': is_mobile(request)})
@login_required(login_url='/api/user_action?action=login')
def file_log(request):
paginator = Paginator(get_file_log(), 20)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'show_file_log.html', {'page_obj':page_obj, 'phone_or_desktop': is_mobile(request)})
@login_required(login_url='/api/user_action?action=login')
def work(request):
username = request.user
u = UserProfile.objects.get(username=username)
show_type = request.GET.get('show_type', '')
show_all = True if show_type == 'admin' and u.is_admin else False
all_info, online_count_all = get_all_info()
single_info, online_count_single = get_single_info(u.id)
paginator = Paginator(all_info, 100) if show_type == 'admin' and u.is_admin else Paginator(single_info, 100)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'show_work.html', {'u':u, 'show_all':show_all, 'page_obj':page_obj, 'online_count_single':online_count_single, 'online_count_all':online_count_all, 'phone_or_desktop': is_mobile(request)})
@login_required(login_url='/api/user_action?action=login')
def down_peers(request):
username = request.user
u = UserProfile.objects.get(username=username)
if not u.is_admin:
print(u.is_admin)
return HttpResponseRedirect('/api/work')
all_info = get_all_info()
f = xlwt.Workbook(encoding='utf-8')
sheet1 = f.add_sheet(_(u'设备信息表'), cell_overwrite_ok=True)
all_fields = [x.name for x in RustDesDevice._meta.get_fields()]
all_fields.append('rust_user')
for i, one in enumerate(all_info):
for j, name in enumerate(all_fields):
if i == 0:
# 写入列名
sheet1.write(i, j, name)
sheet1.write(i+1, j, one.get(name, '-'))
sio = BytesIO()
f.save(sio)
sio.seek(0)
response = HttpResponse(sio.getvalue(), content_type='application/vnd.ms-excel')
response['Content-Disposition'] = 'attachment; filename=DeviceInfo.xls'
response.write(sio.getvalue())
return response
def check_sharelink_expired(sharelink):
now = datetime.datetime.now()
if sharelink.create_time > now:
return False
if (now - sharelink.create_time).seconds <15 * 60:
return False
else:
sharelink.is_expired = True
sharelink.save()
return True
@login_required(login_url='/api/user_action?action=login')
def share(request):
peers = RustDeskPeer.objects.filter(Q(uid=request.user.id))
sharelinks = ShareLink.objects.filter(Q(uid=request.user.id) & Q(is_used=False) & Q(is_expired=False))
# 省资源:处理已过期请求,不主动定时任务轮询请求,在任意地方请求时,检查是否过期,过期则保存。
now = datetime.datetime.now()
for sl in sharelinks:
check_sharelink_expired(sl)
sharelinks = ShareLink.objects.filter(Q(uid=request.user.id) & Q(is_used=False) & Q(is_expired=False))
peers = [{'id':ix+1, 'name':f'{p.rid}|{p.alias}'} for ix, p in enumerate(peers)]
sharelinks = [{'shash':s.shash, 'is_used':s.is_used, 'is_expired':s.is_expired, 'create_time':s.create_time, 'peers':s.peers} for ix, s in enumerate(sharelinks)]
if request.method == 'GET':
url = request.build_absolute_uri()
if url.endswith('share'):
return render(request, 'share.html', {'peers':peers, 'sharelinks':sharelinks})
else:
shash = url.split('/')[-1]
sharelink = ShareLink.objects.filter(Q(shash=shash))
msg = ''
title = 'success'
if not sharelink:
title = 'mistake'
msg = f'Link{url}:<br>Share the link does not exist or have failed.'
else:
sharelink = sharelink[0]
if str(request.user.id) == str(sharelink.uid):
title = 'mistake'
msg = f'Link{url}:<br><br>Lets say, you cant share the link to yourself, right?Intersection'
else:
sharelink.is_used = True
sharelink.save()
peers = sharelink.peers
peers = peers.split(',')
# 自己的peers若重叠需要跳过
peers_self_ids = [x.rid for x in RustDeskPeer.objects.filter(Q(uid=request.user.id))]
peers_share = RustDeskPeer.objects.filter(Q(rid__in=peers) & Q(uid=sharelink.uid))
peers_share_ids = [x.rid for x in peers_share]
for peer in peers_share:
if peer.rid in peers_self_ids:
continue
#peer = RustDeskPeer.objects.get(rid=peer.rid)
peer_f = RustDeskPeer.objects.filter(Q(rid=peer.rid) & Q(uid=sharelink.uid))
if not peer_f:
msg += f"{peer.rid}existed,"
continue
if len(peer_f) > 1:
msg += f'{peer.rid}There are multiple,Has skipped. '
continue
peer = peer_f[0]
peer.id = None
peer.uid = request.user.id
peer.save()
msg += f"{peer.rid},"
msg += 'Has been successfully obtained.'
title = _(title)
msg = _(msg)
return render(request, 'msg.html', {'title':msg, 'msg':msg})
else:
data = request.POST.get('data', '[]')
data = json.loads(data)
if not data:
return JsonResponse({'code':0, 'msg':_('数据为空。')})
rustdesk_ids = [x['title'].split('|')[0] for x in data]
rustdesk_ids = ','.join(rustdesk_ids)
sharelink = ShareLink(
uid=request.user.id,
shash = getStrMd5(str(time.time())+salt),
peers=rustdesk_ids,
)
sharelink.save()
return JsonResponse({'code':1, 'shash':sharelink.shash})
def is_mobile(request):
user_agent = request.META['HTTP_USER_AGENT']
if 'Mobile' in user_agent or 'Android' in user_agent or 'iPhone' in user_agent:
return 'base_phone.html'
else:
return 'base.html'

343
api/views_generator.py Normal file

File diff suppressed because one or more lines are too long

4
db/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

18
docker-compose.yaml Normal file
View File

@@ -0,0 +1,18 @@
version: "3.8"
services:
rustdesk-api-server:
container_name: rustdesk-api-server
build:
context: .
environment:
- HOST=0.0.0.0
- TZ=Asia/Shanghai
- CSRF_TRUSTED_ORIGINS=http://yourdomain.com:21114
volumes:
- /yourpath/db:/rustdesk-api-server/db
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
network_mode: bridge
ports:
- "21114:21114"
restart: unless-stopped

BIN
images/admin_devices.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
images/admin_main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
images/admin_peers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

BIN
images/admin_tags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
images/admin_users.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
images/clients.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
images/connection_log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
images/file_log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
images/front_login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
images/front_main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

BIN
images/front_reg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
images/rust_books.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
images/share.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

BIN
images/user_devices.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
images/webui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
images/windows_run.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

View File

@@ -0,0 +1,592 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-05-14 10:44+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\api\admin_user.py:14 .\api\front_locale.py:28
msgid "密码"
msgstr "Password"
#: .\api\admin_user.py:15
msgid "再次输入密码"
msgstr "Re-enter password"
#: .\api\admin_user.py:26
msgid "密码校验失败,两次密码不一致。"
msgstr "Password verification failed, passwords do not match."
#: .\api\admin_user.py:44
msgid "密码Hash值"
msgstr "Password Hash Value"
#: .\api\admin_user.py:75
msgid "基本信息"
msgstr "Basic Information"
#: .\api\admin_user.py:100
msgid "RustDesk自建Web"
msgstr "RustDesk Self-Hosted Web"
#: .\api\admin_user.py:101
msgid "未定义"
msgstr "Undefined"
#: .\api\front_locale.py:4 .\api\templates\base.html:42
msgid "管理后台"
msgstr "Admin Panel"
#: .\api\front_locale.py:5
msgid "ID列表"
msgstr "ID List"
#: .\api\front_locale.py:6
msgid "分享机器"
msgstr "Share Machine"
#: .\api\front_locale.py:7
msgid "这么简易的东西,忘记密码这功能就没必要了吧。"
msgstr ""
"For such a simple thing, the forgot password feature is unnecessary, right?"
#: .\api\front_locale.py:8
msgid "立即注册"
msgstr "Register Now"
#: .\api\front_locale.py:9
msgid "创建时间"
msgstr "Creation Time"
#: .\api\front_locale.py:10
msgid "注册成功,请前往登录页登录。"
msgstr "Registration successful, please go to the login page to login."
#: .\api\front_locale.py:11
msgid "注册日期"
msgstr "Registration Date"
#: .\api\front_locale.py:12
msgid ""
"2、所分享的机器被分享人享有相同的权限如果机器设置了保存密码被分享人也可"
"以直接连接。"
msgstr ""
"2. The shared machine grants the same permissions to the recipient. If the "
"machine is set to save password, the recipient can also connect directly."
#: .\api\front_locale.py:13
msgid "导出xlsx"
msgstr "Export as xlsx"
#: .\api\front_locale.py:14
msgid "生成分享链接"
msgstr "Generate Share Link"
#: .\api\front_locale.py:15
msgid "请输入8~20位密码。可以包含字母、数字和特殊字符。"
msgstr ""
"Please enter a password of 8~20 characters. It can contain letters, numbers, "
"and special characters."
#: .\api\front_locale.py:16
msgid "尾页"
msgstr "Last Page"
#: .\api\front_locale.py:17
msgid "请确认密码"
msgstr "Please confirm password"
#: .\api\front_locale.py:18
msgid "注册"
msgstr "Register"
#: .\api\front_locale.py:19 .\api\models_work.py:73
msgid "内存"
msgstr "Memory"
#: .\api\front_locale.py:20 .\api\templates\base.html:31
msgid "首页"
msgstr "Home"
#: .\api\front_locale.py:21 .\api\templates\base.html:37
msgid "网页控制"
msgstr "Web Control"
#: .\api\front_locale.py:22
msgid "注册时间"
msgstr "Registration Time"
#: .\api\front_locale.py:23
msgid "链接地址"
msgstr "Link Address"
#: .\api\front_locale.py:24
msgid "请输入密码"
msgstr "Please enter password"
#: .\api\front_locale.py:25 .\api\models_work.py:50 .\api\models_work.py:76
msgid "系统用户名"
msgstr "System Username"
#: .\api\front_locale.py:26
msgid "状态"
msgstr "Status"
#: .\api\front_locale.py:27
msgid "已有账号?立即登录"
msgstr "Already have an account? Login now"
#: .\api\front_locale.py:29 .\api\models_work.py:52
msgid "别名"
msgstr "Alias"
#: .\api\front_locale.py:30
msgid "上一页"
msgstr "Previous Page"
#: .\api\front_locale.py:31
msgid "更新时间"
msgstr "Update Time"
#: .\api\front_locale.py:32
msgid "综合屏"
msgstr "Comprehensive Screen"
#: .\api\front_locale.py:33 .\api\models_work.py:53
msgid "平台"
msgstr "Platform"
#: .\api\front_locale.py:34
msgid "全部用户"
msgstr "All Users"
#: .\api\front_locale.py:35
msgid "注册页"
msgstr "Registration Page"
#: .\api\front_locale.py:36
msgid "分享机器给其他用户"
msgstr "Share machine with other users"
#: .\api\front_locale.py:37 .\api\templates\base.html:33
msgid "所有设备"
msgstr "All Devices"
#: .\api\front_locale.py:38
msgid "连接密码"
msgstr "Connection Password"
#: .\api\front_locale.py:39
msgid "设备统计"
msgstr "Device Statistics"
#: .\api\front_locale.py:40
msgid "所属用户"
msgstr "Belongs to User"
#: .\api\front_locale.py:41 .\api\templates\base.html:36
msgid "分享"
msgstr "Share"
#: .\api\front_locale.py:42
msgid "请输入用户名"
msgstr "Please enter username"
#: .\api\front_locale.py:43
msgid "1、链接有效期为15分钟切勿随意分享给他人。"
msgstr ""
"1. The link is valid for 15 minutes. Do not share it with others casually."
#: .\api\front_locale.py:44
msgid "CPU"
msgstr "CPU"
#: .\api\front_locale.py:45 .\api\models_work.py:49 .\api\models_work.py:70
msgid "客户端ID"
msgstr "Client ID"
#: .\api\front_locale.py:46
msgid "下一页"
msgstr "Next Page"
#: .\api\front_locale.py:47
msgid "登录"
msgstr "Login"
#: .\api\front_locale.py:48 .\api\templates\base.html:45
msgid "退出"
msgstr "Logout"
#: .\api\front_locale.py:49
msgid "请将要分享的机器调整到右侧"
msgstr "Please adjust the machines to be shared to the right"
#: .\api\front_locale.py:50
msgid "成功!如需分享,请复制以下链接给其他人:<br>"
msgstr ""
"Success! If you need to share, please copy the following link to others:<br>"
#: .\api\front_locale.py:51
msgid "忘记密码?"
msgstr "Forgot Password?"
#: .\api\front_locale.py:52
msgid "计算机名"
msgstr "Computer Name"
#: .\api\front_locale.py:53
msgid "两次输入密码不一致!"
msgstr "Passwords do not match!"
#: .\api\front_locale.py:54
msgid "页码"
msgstr "Page Number"
#: .\api\front_locale.py:55
msgid "版本"
msgstr "Version"
#: .\api\front_locale.py:56 .\api\models_user.py:32 .\api\models_work.py:9
msgid "用户名"
msgstr "Username"
#: .\api\front_locale.py:57
msgid ""
"3、为保障安全链接有效期为15分钟、链接仅有效1次。链接一旦被非分享人的登录"
"用户)访问,分享生效,后续访问链接失效。"
msgstr ""
"3. For security reasons, the link is valid for 15 minutes and only valid "
"once. Once the link is accessed by a user (other than the sharing person), "
"the sharing becomes effective, and subsequent access to the link will be "
"invalid."
#: .\api\front_locale.py:58
msgid "系统"
msgstr "System"
#: .\api\front_locale.py:59
msgid "我的机器"
msgstr "My Machine"
#: .\api\front_locale.py:60
#, fuzzy
#| msgid "基本信息"
msgid "信息"
msgstr "Basic Information"
#: .\api\front_locale.py:61
msgid "远程ID"
msgstr "Remote ID"
#: .\api\front_locale.py:62
msgid "远程别名"
msgstr "Remote Alias"
#: .\api\front_locale.py:63 .\api\models_work.py:11 .\api\models_work.py:48
#: .\api\models_work.py:126
msgid "用户ID"
msgstr "User ID"
#: .\api\front_locale.py:64
msgid "用户别名"
msgstr "User Alias"
#: .\api\front_locale.py:65
msgid "用户IP"
msgstr "User IP"
#: .\api\front_locale.py:66
msgid "文件大小"
msgstr "Filesize"
#: .\api\front_locale.py:67
msgid "发送/接受"
msgstr "Sent/Received"
#: .\api\front_locale.py:68
msgid "记录于"
msgstr "Logged At"
#: .\api\front_locale.py:69
msgid "连接开始时间"
msgstr "Connection Start Time\t"
#: .\api\front_locale.py:70
msgid "连接结束时间"
msgstr "Connection End Time"
#: .\api\front_locale.py:71
msgid "时长"
msgstr "Duration"
#: .\api\front_locale.py:72 .\api\templates\base.html:40
msgid "连接日志"
msgstr "Connection Log"
#: .\api\front_locale.py:73 .\api\templates\base.html:41
msgid "文件传输日志"
msgstr "File Transfer Log"
#: .\api\front_locale.py:74
msgid "页码 #"
msgstr "Page #"
#: .\api\front_locale.py:75
msgid "下一页 #"
msgstr "Next #"
#: .\api\front_locale.py:76
msgid "上一页 #"
msgstr "Previous #"
#: .\api\front_locale.py:77
#, fuzzy
#| msgid "上一页"
msgid "第一页"
msgstr "First"
#: .\api\front_locale.py:78
#, fuzzy
#| msgid "上一页"
msgid "上页"
msgstr "Previous Page"
#: .\api\models_user.py:40
msgid "登录信息:"
msgstr "Login Information:"
#: .\api\models_user.py:42
msgid "是否激活"
msgstr "Is Active"
#: .\api\models_user.py:43
msgid "是否管理员"
msgstr "Is Administrator"
#: .\api\models_user.py:82
msgid "用户"
msgstr "User"
#: .\api\models_user.py:83
msgid "用户列表"
msgstr "User List"
#: .\api\models_work.py:10
msgid "RustDesk ID"
msgstr "RustDesk ID"
#: .\api\models_work.py:12
msgid "uuid"
msgstr "UUID"
#: .\api\models_work.py:13
msgid "access_token"
msgstr "Access Token"
#: .\api\models_work.py:14
msgid "登录时间"
msgstr "Login Time"
#: .\api\models_work.py:19
msgid "Token列表"
msgstr "Token List"
#: .\api\models_work.py:30
msgid "所属用户ID"
msgstr "Belongs to User ID"
#: .\api\models_work.py:31
msgid "标签名称"
msgstr "Tag Name"
#: .\api\models_work.py:32
msgid "标签颜色"
msgstr "Tag Color"
#: .\api\models_work.py:37
msgid "Tags列表"
msgstr "Tags List"
#: .\api\models_work.py:51
msgid "操作系统名"
msgstr "Operating System Name"
#: .\api\models_work.py:54
msgid "标签"
msgstr "Tag"
#: .\api\models_work.py:55
msgid "设备链接密码"
msgstr "Device Connection Password"
#: .\api\models_work.py:60
msgid "Peers列表"
msgstr "Peers List"
#: .\api\models_work.py:72
msgid "主机名"
msgstr "Hostname"
#: .\api\models_work.py:74
msgid "操作系统"
msgstr "Operating System"
#: .\api\models_work.py:77
msgid "客户端版本"
msgstr "Client Version"
#: .\api\models_work.py:78
msgid "设备注册时间"
msgstr "Device Registration Time"
#: .\api\models_work.py:83
msgid "设备"
msgstr "Device"
#: .\api\models_work.py:84
msgid "设备列表"
msgstr "Device List"
#: .\api\models_work.py:127
msgid "链接Key"
msgstr "Link Key"
#: .\api\models_work.py:128
msgid "机器ID列表"
msgstr "Machine ID List"
#: .\api\models_work.py:129
msgid "是否使用"
msgstr "Is Used"
#: .\api\models_work.py:130
msgid "是否过期"
msgstr "Is Expired"
#: .\api\models_work.py:131
msgid "生成时间"
msgstr "Generation Time"
#: .\api\models_work.py:137
msgid "分享链接"
msgstr "Share Link"
#: .\api\models_work.py:138
msgid "链接列表"
msgstr "Link List"
#: .\api\views_api.py:20
msgid "请求方式错误请使用POST方式。"
msgstr "Request method error! Please use the POST method."
#: .\api\views_api.py:34
msgid "帐号或密码错误请重试多次重试后将被锁定IP"
msgstr ""
"Account or password error! Please retry. After multiple retries, the IP will "
"be locked!"
#: .\api\views_api.py:72
msgid "请求方式错误!"
msgstr "Request method error!"
#: .\api\views_api.py:80
msgid "异常请求!"
msgstr "Abnormal request!"
#: .\api\views_api.py:93 .\api\views_api.py:213
msgid "错误的提交方式!"
msgstr "Incorrect submission method!"
#: .\api\views_api.py:121
msgid "拉取列表错误!"
msgstr "Error fetching list!"
#: .\api\views_api.py:200
msgid "更新地址簿有误"
msgstr "Error updating address book"
#: .\api\views_api.py:247 .\api\views_front.py:207 .\api\views_front.py:226
msgid "在线"
msgstr "Online"
#: .\api\views_api.py:308
msgid "好的"
msgstr "Okay"
#: .\api\views_front.py:50
msgid "model_to_dict接收的参数必须是模型对象"
msgstr "The parameter received by model_to_dict must be a model object"
#: .\api\views_front.py:55
#, python-brace-format
msgid "model_to_dict,要替换成{replace_field}字段已经存在了"
msgstr ""
"model_to_dict, the field to be replaced with {replace_field} already exists"
#: .\api\views_front.py:60
#, python-brace-format
msgid "model_to_dict,要新增默认值,但字段{default_key}已经存在了"
msgstr ""
"model_to_dict, to add default values, but the field {default_key} already "
"exists"
#: .\api\views_front.py:134
msgid "出了点问题,未获取用户名或密码。"
msgstr "There was a problem, username or password not obtained."
#: .\api\views_front.py:141
msgid "帐号或密码错误!"
msgstr "Account or password error!"
#: .\api\views_front.py:153
msgid "当前未开放注册,请联系管理员!"
msgstr "Registration is currently not open, please contact the administrator!"
#: .\api\views_front.py:160
msgid "用户名不得小于3位"
msgstr "Username must be at least 3 characters"
#: .\api\views_front.py:165
msgid "密码长度不符合要求, 应在8~20位。"
msgstr ""
"Password length does not meet requirements, should be between 8~20 "
"characters."
#: .\api\views_front.py:171
msgid "用户名已存在。"
msgstr "Username already exists."
#: .\api\views_front.py:207 .\api\views_front.py:226
msgid "离线"
msgstr "Offline"
#: .\api\views_front.py:210
msgid "是"
msgstr "Yes"
#: .\api\views_front.py:210
msgid "否"
msgstr "No"
#: .\api\views_front.py:252
msgid "设备信息表"
msgstr "Device Information Table"
#: .\api\views_front.py:351
msgid "数据为空。"
msgstr "Data is empty."
#: .\api\views_front.py:373 .\api\views_front.py:378 .\api\views_front.py:409
#: .\api\views_front.py:414
msgid "UNKNOWN"
msgstr ""
#~ msgid "未知"
#~ msgstr "UNKNOWN"

22
manage.py Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rustdesk_server_api.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
django
xlwt
python-dateutil
psutil
requests
pillow

12
run.sh Normal file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
cd /rustdesk-api-server;
if [ ! -e "./db/db.sqlite3" ]; then
cp "./db_bak/db.sqlite3" "./db/db.sqlite3"
echo "首次运行,初始化数据库"
fi
python manage.py makemigrations
python manage.py migrate
python manage.py runserver $HOST:21114;

View File

View File

@@ -0,0 +1,16 @@
"""
ASGI config for rustdesk_server_api project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rustdesk_server_api.settings')
application = get_asgi_application()

View File

@@ -0,0 +1,177 @@
"""
Django settings for rustdesk_server_api project.
Generated by 'django-admin startproject' using Django 3.1.7.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
if "CSRF_TRUSTED_ORIGINS" in os.environ:
CSRF_TRUSTED_ORIGINS = [os.environ["CSRF_TRUSTED_ORIGINS"]]
else:
CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1:21114"]
SECURE_CROSS_ORIGIN_OPENER_POLICY = 'None'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("SECRET_KEY", 'j%7yjvygpih=6b%qf!q%&ixpn+27dngzdu-i3xh-^3xgy3^nnc')
# ID服务器IP或域名一般与中继服务器用于web client
ID_SERVER = os.environ.get("ID_SERVER", '')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DEBUG", False)
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
ALLOWED_HOSTS = ["*"]
AUTH_USER_MODEL = 'api.UserProfile' # AppName.自定义user
ALLOW_REGISTRATION = os.environ.get("ALLOW_REGISTRATION", "True") # 是否允许注册, True为允许False为不允许
ALLOW_REGISTRATION = True if ALLOW_REGISTRATION.lower() == 'true' else False
GHUSER = os.environ.get("GHUSER", '')
GHBEARER = os.environ.get("GHBEARER", '')
# ==========数据库配置 开始=====================
DATABASE_TYPE = os.environ.get("DATABASE_TYPE", 'SQLITE')
MYSQL_DBNAME = os.environ.get("MYSQL_DBNAME", '-')
MYSQL_HOST = os.environ.get("MYSQL_HOST", '127.0.0.1')
MYSQL_USER = os.environ.get("MYSQL_USER", '-')
MYSQL_PASSWORD = os.environ.get("MYSQL_PASSWORD", '-')
MYSQL_PORT = os.environ.get("MYSQL_PORT", '3306')
# ==========数据库配置 结束=====================
LANGUAGE_CODE = os.environ.get("LANGUAGE_CODE", 'zh-hans')
# #LANGUAGE_CODE = os.environ.get("LANGUAGE_CODE", 'en')
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api',
'webui',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware', # 取消post的验证。
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'rustdesk_server_api.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'api.util.settings',
],
},
},
]
WSGI_APPLICATION = 'rustdesk_server_api.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db/db.sqlite3',
},
}
if DATABASE_TYPE == 'MYSQL' and MYSQL_DBNAME != '-' and MYSQL_USER != '-' and MYSQL_PASSWORD != '-':
# 简单通过数据库名、账密信息过滤下防止用户未配置mysql却使用mysql
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': MYSQL_DBNAME, # 数据库名
'HOST': MYSQL_HOST, # 数据库服务器IP
'USER': MYSQL_USER, # 数据库用户名
'PASSWORD': MYSQL_PASSWORD, # 数据库密码
'PORT': MYSQL_PORT, # 端口
'OPTIONS': {'charset': 'utf8'},
}
}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en'
TIME_ZONE = 'America/Chicago'
USE_I18N = True
USE_L10N = True
# USE_TZ = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = 'static/'
if DEBUG:
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
else:
STATIC_ROOT = os.path.join(BASE_DIR, 'static') # 新增
LANGUAGES = (
('zh-hans', '中文简体'),
('en', 'English'),
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)

View File

@@ -0,0 +1,43 @@
"""rustdesk_server_api URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
import django
from django.contrib import admin
from django.urls import path
from api.views import index
if django.__version__.split('.')[0]>='4':
from django.urls import re_path as url
from django.conf.urls import include
else:
from django.conf.urls import url, include
from django.views import static ##新增
from django.conf import settings
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
path('admin/', admin.site.urls),
url(r'^$', index),
url(r'^api/', include('api.urls')),
url(r'^webui/', include('webui.urls')),
url(r'^static/(?P<path>.*)$', static.serve, {'document_root': settings.STATIC_ROOT}, name='static'),
url(r'^canvaskit@0.33.0/(?P<path>.*)$', static.serve, {'document_root': 'static/web_client/canvaskit@0.33.0'},name='web_client'),
]
from django.conf.urls import static as sc
if not settings.DEBUG:
urlpatterns += sc.static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@@ -0,0 +1,16 @@
"""
WSGI config for rustdesk_server_api project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rustdesk_server_api.settings')
application = get_wsgi_application()

View File

@@ -0,0 +1,275 @@
select.admin-autocomplete {
width: 20em;
}
.select2-container--admin-autocomplete.select2-container {
min-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single,
.select2-container--admin-autocomplete .select2-selection--multiple {
min-height: 30px;
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
border-color: var(--body-quiet-color);
min-height: 30px;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
padding: 0;
}
.select2-container--admin-autocomplete .select2-selection--single {
background-color: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
color: var(--body-fg);
line-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
color: var(--body-quiet-color);
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
background-color: var(--darkened-bg);
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px;
}
.select2-container--admin-autocomplete .select2-selection--multiple {
background-color: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: text;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 10px 5px 5px;
width: 100%;
display: flex;
flex-wrap: wrap;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
list-style: none;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
color: var(--body-quiet-color);
margin-top: 5px;
float: left;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin: 5px;
position: absolute;
right: 0;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
background-color: var(--darkened-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
color: var(--body-quiet-color);
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
color: var(--body-fg);
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
border: solid var(--body-quiet-color) 1px;
outline: 0;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
background-color: var(--darkened-bg);
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.select2-container--admin-autocomplete .select2-search--dropdown {
background: var(--darkened-bg);
}
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
background: var(--body-bg);
color: var(--body-fg);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
background: transparent;
color: var(--body-fg);
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield;
}
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto;
color: var(--body-fg);
background: var(--body-bg);
}
.select2-container--admin-autocomplete .select2-results__option[role=group] {
padding: 0;
}
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
color: var(--body-quiet-color);
}
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
background-color: var(--selected-bg);
color: var(--body-fg);
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
padding-left: 1em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -4em;
padding-left: 5em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -5em;
padding-left: 6em;
}
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
background-color: var(--primary);
color: var(--primary-fg);
}
.select2-container--admin-autocomplete .select2-results__group {
cursor: default;
display: block;
padding: 6px;
}

1138
static/admin/css/base.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,328 @@
/* CHANGELISTS */
#changelist {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
#changelist .changelist-form-container {
flex: 1 1 auto;
min-width: 0;
}
#changelist table {
width: 100%;
}
.change-list .hiddenfields { display:none; }
.change-list .filtered table {
border-right: none;
}
.change-list .filtered {
min-height: 400px;
}
.change-list .filtered .results, .change-list .filtered .paginator,
.filtered #toolbar, .filtered div.xfull {
width: auto;
}
.change-list .filtered table tbody th {
padding-right: 1em;
}
#changelist-form .results {
overflow-x: auto;
width: 100%;
}
#changelist .toplinks {
border-bottom: 1px solid var(--hairline-color);
}
#changelist .paginator {
color: var(--body-quiet-color);
border-bottom: 1px solid var(--hairline-color);
background: var(--body-bg);
overflow: hidden;
}
/* CHANGELIST TABLES */
#changelist table thead th {
padding: 0;
white-space: nowrap;
vertical-align: middle;
}
#changelist table thead th.action-checkbox-column {
width: 1.5em;
text-align: center;
}
#changelist table tbody td.action-checkbox {
text-align: center;
}
#changelist table tfoot {
color: var(--body-quiet-color);
}
/* TOOLBAR */
#toolbar {
padding: 8px 10px;
margin-bottom: 15px;
border-top: 1px solid var(--hairline-color);
border-bottom: 1px solid var(--hairline-color);
background: var(--darkened-bg);
color: var(--body-quiet-color);
}
#toolbar form input {
border-radius: 4px;
font-size: 0.875rem;
padding: 5px;
color: var(--body-fg);
}
#toolbar #searchbar {
height: 1.1875rem;
border: 1px solid var(--border-color);
padding: 2px 5px;
margin: 0;
vertical-align: top;
font-size: 0.8125rem;
max-width: 100%;
}
#toolbar #searchbar:focus {
border-color: var(--body-quiet-color);
}
#toolbar form input[type="submit"] {
border: 1px solid var(--border-color);
font-size: 0.8125rem;
padding: 4px 8px;
margin: 0;
vertical-align: middle;
background: var(--body-bg);
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
cursor: pointer;
color: var(--body-fg);
}
#toolbar form input[type="submit"]:focus,
#toolbar form input[type="submit"]:hover {
border-color: var(--body-quiet-color);
}
#changelist-search img {
vertical-align: middle;
margin-right: 4px;
}
#changelist-search .help {
word-break: break-word;
}
/* FILTER COLUMN */
#changelist-filter {
flex: 0 0 240px;
order: 1;
background: var(--darkened-bg);
border-left: none;
margin: 0 0 0 30px;
}
#changelist-filter h2 {
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 5px 15px;
margin-bottom: 12px;
border-bottom: none;
}
#changelist-filter h3,
#changelist-filter details summary {
font-weight: 400;
padding: 0 15px;
margin-bottom: 10px;
}
#changelist-filter details summary > * {
display: inline;
}
#changelist-filter details > summary {
list-style-type: none;
}
#changelist-filter details > summary::-webkit-details-marker {
display: none;
}
#changelist-filter details > summary::before {
content: '→';
font-weight: bold;
color: var(--link-hover-color);
}
#changelist-filter details[open] > summary::before {
content: '↓';
}
#changelist-filter ul {
margin: 5px 0;
padding: 0 15px 15px;
border-bottom: 1px solid var(--hairline-color);
}
#changelist-filter ul:last-child {
border-bottom: none;
}
#changelist-filter li {
list-style-type: none;
margin-left: 0;
padding-left: 0;
}
#changelist-filter a {
display: block;
color: var(--body-quiet-color);
word-break: break-word;
}
#changelist-filter li.selected {
border-left: 5px solid var(--hairline-color);
padding-left: 10px;
margin-left: -15px;
}
#changelist-filter li.selected a {
color: var(--link-selected-fg);
}
#changelist-filter a:focus, #changelist-filter a:hover,
#changelist-filter li.selected a:focus,
#changelist-filter li.selected a:hover {
color: var(--link-hover-color);
}
#changelist-filter #changelist-filter-clear a {
font-size: 0.8125rem;
padding-bottom: 10px;
border-bottom: 1px solid var(--hairline-color);
}
/* DATE DRILLDOWN */
.change-list .toplinks {
display: flex;
padding-bottom: 5px;
flex-wrap: wrap;
gap: 3px 17px;
font-weight: bold;
}
.change-list .toplinks a {
font-size: 0.8125rem;
}
.change-list .toplinks .date-back {
color: var(--body-quiet-color);
}
.change-list .toplinks .date-back:focus,
.change-list .toplinks .date-back:hover {
color: var(--link-hover-color);
}
/* ACTIONS */
.filtered .actions {
border-right: none;
}
#changelist table input {
margin: 0;
vertical-align: baseline;
}
/* Once the :has() pseudo-class is supported by all browsers, the tr.selected
selector and the JS adding the class can be removed. */
#changelist tbody tr.selected {
background-color: var(--selected-row);
}
#changelist tbody tr:has(.action-select:checked) {
background-color: var(--selected-row);
}
#changelist .actions {
padding: 10px;
background: var(--body-bg);
border-top: none;
border-bottom: none;
line-height: 1.5rem;
color: var(--body-quiet-color);
width: 100%;
}
#changelist .actions span.all,
#changelist .actions span.action-counter,
#changelist .actions span.clear,
#changelist .actions span.question {
font-size: 0.8125rem;
margin: 0 0.5em;
}
#changelist .actions:last-child {
border-bottom: none;
}
#changelist .actions select {
vertical-align: top;
height: 1.5rem;
color: var(--body-fg);
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 0.875rem;
padding: 0 0 0 4px;
margin: 0;
margin-left: 10px;
}
#changelist .actions select:focus {
border-color: var(--body-quiet-color);
}
#changelist .actions label {
display: inline-block;
vertical-align: middle;
font-size: 0.8125rem;
}
#changelist .actions .button {
font-size: 0.8125rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--body-bg);
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
cursor: pointer;
height: 1.5rem;
line-height: 1;
padding: 4px 8px;
margin: 0;
color: var(--body-fg);
}
#changelist .actions .button:focus, #changelist .actions .button:hover {
border-color: var(--body-quiet-color);
}

View File

@@ -0,0 +1,137 @@
@media (prefers-color-scheme: dark) {
:root {
--primary: #264b5d;
--primary-fg: #f7f7f7;
--body-fg: #eeeeee;
--body-bg: #121212;
--body-quiet-color: #e0e0e0;
--body-loud-color: #ffffff;
--breadcrumbs-link-fg: #e0e0e0;
--breadcrumbs-bg: var(--primary);
--link-fg: #81d4fa;
--link-hover-color: #4ac1f7;
--link-selected-fg: #6f94c6;
--hairline-color: #272727;
--border-color: #353535;
--error-fg: #e35f5f;
--message-success-bg: #006b1b;
--message-warning-bg: #583305;
--message-error-bg: #570808;
--darkened-bg: #212121;
--selected-bg: #1b1b1b;
--selected-row: #00363a;
--close-button-bg: #333333;
--close-button-hover-bg: #666666;
}
}
html[data-theme="dark"] {
--primary: #264b5d;
--primary-fg: #f7f7f7;
--body-fg: #eeeeee;
--body-bg: #121212;
--body-quiet-color: #e0e0e0;
--body-loud-color: #ffffff;
--breadcrumbs-link-fg: #e0e0e0;
--breadcrumbs-bg: var(--primary);
--link-fg: #81d4fa;
--link-hover-color: #4ac1f7;
--link-selected-fg: #6f94c6;
--hairline-color: #272727;
--border-color: #353535;
--error-fg: #e35f5f;
--message-success-bg: #006b1b;
--message-warning-bg: #583305;
--message-error-bg: #570808;
--darkened-bg: #212121;
--selected-bg: #1b1b1b;
--selected-row: #00363a;
--close-button-bg: #333333;
--close-button-hover-bg: #666666;
}
/* THEME SWITCH */
.theme-toggle {
cursor: pointer;
border: none;
padding: 0;
background: transparent;
vertical-align: middle;
margin-inline-start: 5px;
margin-top: -1px;
}
.theme-toggle svg {
vertical-align: middle;
height: 1rem;
width: 1rem;
display: none;
}
/*
Fully hide screen reader text so we only show the one matching the current
theme.
*/
.theme-toggle .visually-hidden {
display: none;
}
html[data-theme="auto"] .theme-toggle .theme-label-when-auto {
display: block;
}
html[data-theme="dark"] .theme-toggle .theme-label-when-dark {
display: block;
}
html[data-theme="light"] .theme-toggle .theme-label-when-light {
display: block;
}
/* ICONS */
.theme-toggle svg.theme-icon-when-auto,
.theme-toggle svg.theme-icon-when-dark,
.theme-toggle svg.theme-icon-when-light {
fill: var(--header-link-color);
color: var(--header-bg);
}
html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto {
display: block;
}
html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark {
display: block;
}
html[data-theme="light"] .theme-toggle svg.theme-icon-when-light {
display: block;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
color: var(--body-fg);
background-color: var(--body-bg);
}

View File

@@ -0,0 +1,29 @@
/* DASHBOARD */
.dashboard td, .dashboard th {
word-break: break-word;
}
.dashboard .module table th {
width: 100%;
}
.dashboard .module table td {
white-space: nowrap;
}
.dashboard .module table td a {
display: block;
padding-right: .6em;
}
/* RECENT ACTIONS MODULE */
.module ul.actionlist {
margin-left: 0;
}
ul.actionlist li {
list-style-type: none;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -0,0 +1,20 @@
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Bold-webfont.woff');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Regular-webfont.woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto-Light-webfont.woff');
font-weight: 300;
font-style: normal;
}

530
static/admin/css/forms.css Normal file
View File

@@ -0,0 +1,530 @@
@import url('widgets.css');
/* FORM ROWS */
.form-row {
overflow: hidden;
padding: 10px;
font-size: 0.8125rem;
border-bottom: 1px solid var(--hairline-color);
}
.form-row img, .form-row input {
vertical-align: middle;
}
.form-row label input[type="checkbox"] {
margin-top: 0;
vertical-align: 0;
}
form .form-row p {
padding-left: 0;
}
.flex-container {
display: flex;
flex-wrap: wrap;
}
.form-multiline > div {
padding-bottom: 10px;
}
/* FORM LABELS */
label {
font-weight: normal;
color: var(--body-quiet-color);
font-size: 0.8125rem;
}
.required label, label.required {
font-weight: bold;
color: var(--body-fg);
}
/* RADIO BUTTONS */
form div.radiolist div {
padding-right: 7px;
}
form div.radiolist.inline div {
display: inline-block;
}
form div.radiolist label {
width: auto;
}
form div.radiolist input[type="radio"] {
margin: -2px 4px 0 0;
padding: 0;
}
form ul.inline {
margin-left: 0;
padding: 0;
}
form ul.inline li {
float: left;
padding-right: 7px;
}
/* ALIGNED FIELDSETS */
.aligned label {
display: block;
padding: 4px 10px 0 0;
width: 160px;
word-wrap: break-word;
line-height: 1;
}
.aligned label:not(.vCheckboxLabel):after {
content: '';
display: inline-block;
vertical-align: middle;
height: 1.625rem;
}
.aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly {
padding: 6px 0;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
overflow-wrap: break-word;
}
.aligned ul label {
display: inline;
float: none;
width: auto;
}
.aligned .form-row input {
margin-bottom: 0;
}
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
width: 350px;
}
form .aligned ul {
margin-left: 160px;
padding-left: 10px;
}
form .aligned div.radiolist {
display: inline-block;
margin: 0;
padding: 0;
}
form .aligned p.help,
form .aligned div.help {
margin-top: 0;
margin-left: 160px;
padding-left: 10px;
}
form .aligned p.date div.help.timezonewarning,
form .aligned p.datetime div.help.timezonewarning,
form .aligned p.time div.help.timezonewarning {
margin-left: 0;
padding-left: 0;
font-weight: normal;
}
form .aligned p.help:last-child,
form .aligned div.help:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
form .aligned input + p.help,
form .aligned textarea + p.help,
form .aligned select + p.help,
form .aligned input + div.help,
form .aligned textarea + div.help,
form .aligned select + div.help {
margin-left: 160px;
padding-left: 10px;
}
form .aligned ul li {
list-style: none;
}
form .aligned table p {
margin-left: 0;
padding-left: 0;
}
.aligned .vCheckboxLabel {
float: none;
width: auto;
display: inline-block;
vertical-align: -3px;
padding: 0 0 5px 5px;
}
.aligned .vCheckboxLabel + p.help,
.aligned .vCheckboxLabel + div.help {
margin-top: -4px;
}
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
width: 610px;
}
fieldset .fieldBox {
margin-right: 20px;
}
/* WIDE FIELDSETS */
.wide label {
width: 200px;
}
form .wide p,
form .wide ul.errorlist,
form .wide input + p.help,
form .wide input + div.help {
margin-left: 200px;
}
form .wide p.help,
form .wide div.help {
padding-left: 50px;
}
form div.help ul {
padding-left: 0;
margin-left: 0;
}
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
width: 450px;
}
/* COLLAPSED FIELDSETS */
fieldset.collapsed * {
display: none;
}
fieldset.collapsed h2, fieldset.collapsed {
display: block;
}
fieldset.collapsed {
border: 1px solid var(--hairline-color);
border-radius: 4px;
overflow: hidden;
}
fieldset.collapsed h2 {
background: var(--darkened-bg);
color: var(--body-quiet-color);
}
fieldset .collapse-toggle {
color: var(--header-link-color);
}
fieldset.collapsed .collapse-toggle {
background: transparent;
display: inline;
color: var(--link-fg);
}
/* MONOSPACE TEXTAREAS */
fieldset.monospace textarea {
font-family: var(--font-family-monospace);
}
/* SUBMIT ROW */
.submit-row {
padding: 12px 14px 12px;
margin: 0 0 20px;
background: var(--darkened-bg);
border: 1px solid var(--hairline-color);
border-radius: 4px;
overflow: hidden;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
body.popup .submit-row {
overflow: auto;
}
.submit-row input {
height: 2.1875rem;
line-height: 0.9375rem;
}
.submit-row input, .submit-row a {
margin: 0;
}
.submit-row input.default {
text-transform: uppercase;
}
.submit-row a.deletelink {
margin-left: auto;
}
.submit-row a.deletelink {
display: block;
background: var(--delete-button-bg);
border-radius: 4px;
padding: 0.625rem 0.9375rem;
height: 0.9375rem;
line-height: 0.9375rem;
color: var(--button-fg);
}
.submit-row a.closelink {
display: inline-block;
background: var(--close-button-bg);
border-radius: 4px;
padding: 10px 15px;
height: 0.9375rem;
line-height: 0.9375rem;
color: var(--button-fg);
}
.submit-row a.deletelink:focus,
.submit-row a.deletelink:hover,
.submit-row a.deletelink:active {
background: var(--delete-button-hover-bg);
text-decoration: none;
}
.submit-row a.closelink:focus,
.submit-row a.closelink:hover,
.submit-row a.closelink:active {
background: var(--close-button-hover-bg);
text-decoration: none;
}
/* CUSTOM FORM FIELDS */
.vSelectMultipleField {
vertical-align: top;
}
.vCheckboxField {
border: none;
}
.vDateField, .vTimeField {
margin-right: 2px;
margin-bottom: 4px;
}
.vDateField {
min-width: 6.85em;
}
.vTimeField {
min-width: 4.7em;
}
.vURLField {
width: 30em;
}
.vLargeTextField, .vXMLLargeTextField {
width: 48em;
}
.flatpages-flatpage #id_content {
height: 40.2em;
}
.module table .vPositiveSmallIntegerField {
width: 2.2em;
}
.vIntegerField {
width: 5em;
}
.vBigIntegerField {
width: 10em;
}
.vForeignKeyRawIdAdminField {
width: 5em;
}
.vTextField, .vUUIDField {
width: 20em;
}
/* INLINES */
.inline-group {
padding: 0;
margin: 0 0 30px;
}
.inline-group thead th {
padding: 8px 10px;
}
.inline-group .aligned label {
width: 160px;
}
.inline-related {
position: relative;
}
.inline-related h3 {
margin: 0;
color: var(--body-quiet-color);
padding: 5px;
font-size: 0.8125rem;
background: var(--darkened-bg);
border-top: 1px solid var(--hairline-color);
border-bottom: 1px solid var(--hairline-color);
}
.inline-related h3 span.delete {
float: right;
}
.inline-related h3 span.delete label {
margin-left: 2px;
font-size: 0.6875rem;
}
.inline-related fieldset {
margin: 0;
background: var(--body-bg);
border: none;
width: 100%;
}
.inline-related fieldset.module h3 {
margin: 0;
padding: 2px 5px 3px 5px;
font-size: 0.6875rem;
text-align: left;
font-weight: bold;
background: #bcd;
color: var(--body-bg);
}
.inline-group .tabular fieldset.module {
border: none;
}
.inline-related.tabular fieldset.module table {
width: 100%;
overflow-x: scroll;
}
.last-related fieldset {
border: none;
}
.inline-group .tabular tr.has_original td {
padding-top: 2em;
}
.inline-group .tabular tr td.original {
padding: 2px 0 0 0;
width: 0;
_position: relative;
}
.inline-group .tabular th.original {
width: 0px;
padding: 0;
}
.inline-group .tabular td.original p {
position: absolute;
left: 0;
height: 1.1em;
padding: 2px 9px;
overflow: hidden;
font-size: 0.5625rem;
font-weight: bold;
color: var(--body-quiet-color);
_width: 700px;
}
.inline-group ul.tools {
padding: 0;
margin: 0;
list-style: none;
}
.inline-group ul.tools li {
display: inline;
padding: 0 5px;
}
.inline-group div.add-row,
.inline-group .tabular tr.add-row td {
color: var(--body-quiet-color);
background: var(--darkened-bg);
padding: 8px 10px;
border-bottom: 1px solid var(--hairline-color);
}
.inline-group .tabular tr.add-row td {
padding: 8px 10px;
border-bottom: 1px solid var(--hairline-color);
}
.inline-group ul.tools a.add,
.inline-group div.add-row a,
.inline-group .tabular tr.add-row td a {
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
padding-left: 16px;
font-size: 0.75rem;
}
.empty-form {
display: none;
}
/* RELATED FIELD ADD ONE / LOOKUP */
.related-lookup {
margin-left: 5px;
display: inline-block;
vertical-align: middle;
background-repeat: no-repeat;
background-size: 14px;
}
.related-lookup {
width: 1rem;
height: 1rem;
background-image: url(../img/search.svg);
}
form .related-widget-wrapper ul {
display: inline-block;
margin-left: 0;
padding-left: 0;
}
.clearable-file-input input {
margin-top: 0;
}

View File

@@ -0,0 +1,61 @@
/* LOGIN FORM */
.login {
background: var(--darkened-bg);
height: auto;
}
.login #header {
height: auto;
padding: 15px 16px;
justify-content: center;
}
.login #header h1 {
font-size: 1.125rem;
margin: 0;
}
.login #header h1 a {
color: var(--header-link-color);
}
.login #content {
padding: 20px 20px 0;
}
.login #container {
background: var(--body-bg);
border: 1px solid var(--hairline-color);
border-radius: 4px;
overflow: hidden;
width: 28em;
min-width: 300px;
margin: 100px auto;
height: auto;
}
.login .form-row {
padding: 4px 0;
}
.login .form-row label {
display: block;
line-height: 2em;
}
.login .form-row #id_username, .login .form-row #id_password {
padding: 8px;
width: 100%;
box-sizing: border-box;
}
.login .submit-row {
padding: 1em 0 0 0;
margin: 0;
text-align: center;
}
.login .password-reset-link {
text-align: center;
}

View File

@@ -0,0 +1,144 @@
.sticky {
position: sticky;
top: 0;
max-height: 100vh;
}
.toggle-nav-sidebar {
z-index: 20;
left: 0;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 23px;
width: 23px;
border: 0;
border-right: 1px solid var(--hairline-color);
background-color: var(--body-bg);
cursor: pointer;
font-size: 1.25rem;
color: var(--link-fg);
padding: 0;
}
[dir="rtl"] .toggle-nav-sidebar {
border-left: 1px solid var(--hairline-color);
border-right: 0;
}
.toggle-nav-sidebar:hover,
.toggle-nav-sidebar:focus {
background-color: var(--darkened-bg);
}
#nav-sidebar {
z-index: 15;
flex: 0 0 275px;
left: -276px;
margin-left: -276px;
border-top: 1px solid transparent;
border-right: 1px solid var(--hairline-color);
background-color: var(--body-bg);
overflow: auto;
}
[dir="rtl"] #nav-sidebar {
border-left: 1px solid var(--hairline-color);
border-right: 0;
left: 0;
margin-left: 0;
right: -276px;
margin-right: -276px;
}
.toggle-nav-sidebar::before {
content: '\00BB';
}
.main.shifted .toggle-nav-sidebar::before {
content: '\00AB';
}
.main > #nav-sidebar {
visibility: hidden;
}
.main.shifted > #nav-sidebar {
margin-left: 0;
visibility: visible;
}
[dir="rtl"] .main.shifted > #nav-sidebar {
margin-right: 0;
}
#nav-sidebar .module th {
width: 100%;
overflow-wrap: anywhere;
}
#nav-sidebar .module th,
#nav-sidebar .module caption {
padding-left: 16px;
}
#nav-sidebar .module td {
white-space: nowrap;
}
[dir="rtl"] #nav-sidebar .module th,
[dir="rtl"] #nav-sidebar .module caption {
padding-left: 8px;
padding-right: 16px;
}
#nav-sidebar .current-app .section:link,
#nav-sidebar .current-app .section:visited {
color: var(--header-color);
font-weight: bold;
}
#nav-sidebar .current-model {
background: var(--selected-row);
}
.main > #nav-sidebar + .content {
max-width: calc(100% - 23px);
}
.main.shifted > #nav-sidebar + .content {
max-width: calc(100% - 299px);
}
@media (max-width: 767px) {
#nav-sidebar, #toggle-nav-sidebar {
display: none;
}
.main > #nav-sidebar + .content,
.main.shifted > #nav-sidebar + .content {
max-width: 100%;
}
}
#nav-filter {
width: 100%;
box-sizing: border-box;
padding: 2px 5px;
margin: 5px 0;
border: 1px solid var(--border-color);
background-color: var(--darkened-bg);
color: var(--body-fg);
}
#nav-filter:focus {
border-color: var(--body-quiet-color);
}
#nav-filter.no-results {
background: var(--message-error-bg);
}
#nav-sidebar table {
width: 100%;
}

View File

@@ -0,0 +1,998 @@
/* Tablets */
input[type="submit"], button {
-webkit-appearance: none;
appearance: none;
}
@media (max-width: 1024px) {
/* Basic */
html {
-webkit-text-size-adjust: 100%;
}
td, th {
padding: 10px;
font-size: 0.875rem;
}
.small {
font-size: 0.75rem;
}
/* Layout */
#container {
min-width: 0;
}
#content {
padding: 15px 20px 20px;
}
div.breadcrumbs {
padding: 10px 30px;
}
/* Header */
#header {
flex-direction: column;
padding: 15px 30px;
justify-content: flex-start;
}
#branding h1 {
margin: 0 0 8px;
line-height: 1.2;
}
#user-tools {
margin: 0;
font-weight: 400;
line-height: 1.85;
text-align: left;
}
#user-tools a {
display: inline-block;
line-height: 1.4;
}
/* Dashboard */
.dashboard #content {
width: auto;
}
#content-related {
margin-right: -290px;
}
.colSM #content-related {
margin-left: -290px;
}
.colMS {
margin-right: 290px;
}
.colSM {
margin-left: 290px;
}
.dashboard .module table td a {
padding-right: 0;
}
td .changelink, td .addlink {
font-size: 0.8125rem;
}
/* Changelist */
#toolbar {
border: none;
padding: 15px;
}
#changelist-search > div {
display: flex;
flex-wrap: nowrap;
max-width: 480px;
}
#changelist-search label {
line-height: 1.375rem;
}
#toolbar form #searchbar {
flex: 1 0 auto;
width: 0;
height: 1.375rem;
margin: 0 10px 0 6px;
}
#toolbar form input[type=submit] {
flex: 0 1 auto;
}
#changelist-search .quiet {
width: 0;
flex: 1 0 auto;
margin: 5px 0 0 25px;
}
#changelist .actions {
display: flex;
flex-wrap: wrap;
padding: 15px 0;
}
#changelist .actions label {
display: flex;
}
#changelist .actions select {
background: var(--body-bg);
}
#changelist .actions .button {
min-width: 48px;
margin: 0 10px;
}
#changelist .actions span.all,
#changelist .actions span.clear,
#changelist .actions span.question,
#changelist .actions span.action-counter {
font-size: 0.6875rem;
margin: 0 10px 0 0;
}
#changelist-filter {
flex-basis: 200px;
}
.change-list .filtered .results,
.change-list .filtered .paginator,
.filtered #toolbar,
.filtered .actions,
#changelist .paginator {
border-top-color: var(--hairline-color); /* XXX Is this used at all? */
}
#changelist .results + .paginator {
border-top: none;
}
/* Forms */
label {
font-size: 0.875rem;
}
.form-row input[type=text],
.form-row input[type=password],
.form-row input[type=email],
.form-row input[type=url],
.form-row input[type=tel],
.form-row input[type=number],
.form-row textarea,
.form-row select,
.form-row .vTextField {
box-sizing: border-box;
margin: 0;
padding: 6px 8px;
min-height: 2.25rem;
font-size: 0.875rem;
}
.form-row select {
height: 2.25rem;
}
.form-row select[multiple] {
height: auto;
min-height: 0;
}
fieldset .fieldBox + .fieldBox {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--hairline-color);
}
textarea {
max-width: 100%;
max-height: 120px;
}
.aligned label {
padding-top: 6px;
}
.aligned .related-lookup,
.aligned .datetimeshortcuts,
.aligned .related-lookup + strong {
align-self: center;
margin-left: 15px;
}
form .aligned div.radiolist {
margin-left: 2px;
}
.submit-row {
padding: 8px;
}
.submit-row a.deletelink {
padding: 10px 7px;
}
.button, input[type=submit], input[type=button], .submit-row input, a.button {
padding: 7px;
}
/* Related widget */
.related-widget-wrapper {
float: none;
}
.related-widget-wrapper-link + .selector {
max-width: calc(100% - 30px);
margin-right: 15px;
}
select + .related-widget-wrapper-link,
.related-widget-wrapper-link + .related-widget-wrapper-link {
margin-left: 10px;
}
/* Selector */
.selector {
display: flex;
width: 100%;
}
.selector .selector-filter {
display: flex;
align-items: center;
}
.selector .selector-filter label {
margin: 0 8px 0 0;
}
.selector .selector-filter input {
width: auto;
min-height: 0;
flex: 1 1;
}
.selector-available, .selector-chosen {
width: auto;
flex: 1 1;
display: flex;
flex-direction: column;
}
.selector select {
width: 100%;
flex: 1 0 auto;
margin-bottom: 5px;
}
.selector ul.selector-chooser {
width: 26px;
height: 52px;
padding: 2px 0;
margin: auto 15px;
border-radius: 20px;
transform: translateY(-10px);
}
.selector-add, .selector-remove {
width: 20px;
height: 20px;
background-size: 20px auto;
}
.selector-add {
background-position: 0 -120px;
}
.selector-remove {
background-position: 0 -80px;
}
a.selector-chooseall, a.selector-clearall {
align-self: center;
}
.stacked {
flex-direction: column;
max-width: 480px;
}
.stacked > * {
flex: 0 1 auto;
}
.stacked select {
margin-bottom: 0;
}
.stacked .selector-available, .stacked .selector-chosen {
width: auto;
}
.stacked ul.selector-chooser {
width: 52px;
height: 26px;
padding: 0 2px;
margin: 15px auto;
transform: none;
}
.stacked .selector-chooser li {
padding: 3px;
}
.stacked .selector-add, .stacked .selector-remove {
background-size: 20px auto;
}
.stacked .selector-add {
background-position: 0 -40px;
}
.stacked .active.selector-add {
background-position: 0 -40px;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -140px;
}
.stacked .active.selector-add:focus, .stacked .active.selector-add:hover {
background-position: 0 -60px;
}
.stacked .selector-remove {
background-position: 0 0;
}
.stacked .active.selector-remove {
background-position: 0 0;
}
.active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -100px;
}
.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover {
background-position: 0 -20px;
}
.help-tooltip, .selector .help-icon {
display: none;
}
.datetime input {
width: 50%;
max-width: 120px;
}
.datetime span {
font-size: 0.8125rem;
}
.datetime .timezonewarning {
display: block;
font-size: 0.6875rem;
color: var(--body-quiet-color);
}
.datetimeshortcuts {
color: var(--border-color); /* XXX Redundant, .datetime span also sets #ccc */
}
.form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
width: 75%;
}
.inline-group {
overflow: auto;
}
/* Messages */
ul.messagelist li {
padding-left: 55px;
background-position: 30px 12px;
}
ul.messagelist li.error {
background-position: 30px 12px;
}
ul.messagelist li.warning {
background-position: 30px 14px;
}
/* Login */
.login #header {
padding: 15px 20px;
}
.login #branding h1 {
margin: 0;
}
/* GIS */
div.olMap {
max-width: calc(100vw - 30px);
max-height: 300px;
}
.olMap + .clear_features {
display: block;
margin-top: 10px;
}
/* Docs */
.module table.xfull {
width: 100%;
}
pre.literal-block {
overflow: auto;
}
}
/* Mobile */
@media (max-width: 767px) {
/* Layout */
#header, #content, #footer {
padding: 15px;
}
#footer:empty {
padding: 0;
}
div.breadcrumbs {
padding: 10px 15px;
}
/* Dashboard */
.colMS, .colSM {
margin: 0;
}
#content-related, .colSM #content-related {
width: 100%;
margin: 0;
}
#content-related .module {
margin-bottom: 0;
}
#content-related .module h2 {
padding: 10px 15px;
font-size: 1rem;
}
/* Changelist */
#changelist {
align-items: stretch;
flex-direction: column;
}
#toolbar {
padding: 10px;
}
#changelist-filter {
margin-left: 0;
}
#changelist .actions label {
flex: 1 1;
}
#changelist .actions select {
flex: 1 0;
width: 100%;
}
#changelist .actions span {
flex: 1 0 100%;
}
#changelist-filter {
position: static;
width: auto;
margin-top: 30px;
}
.object-tools {
float: none;
margin: 0 0 15px;
padding: 0;
overflow: hidden;
}
.object-tools li {
height: auto;
margin-left: 0;
}
.object-tools li + li {
margin-left: 15px;
}
/* Forms */
.form-row {
padding: 15px 0;
}
.aligned .form-row,
.aligned .form-row > div {
max-width: 100vw;
}
.aligned .form-row > div {
width: calc(100vw - 30px);
}
.flex-container {
flex-flow: column;
}
textarea {
max-width: none;
}
.vURLField {
width: auto;
}
fieldset .fieldBox + .fieldBox {
margin-top: 15px;
padding-top: 15px;
}
fieldset.collapsed .form-row {
display: none;
}
.aligned label {
width: 100%;
padding: 0 0 10px;
}
.aligned label:after {
max-height: 0;
}
.aligned .form-row input,
.aligned .form-row select,
.aligned .form-row textarea {
flex: 1 1 auto;
max-width: 100%;
}
.aligned .checkbox-row {
align-items: center;
}
.aligned .checkbox-row input {
flex: 0 1 auto;
margin: 0;
}
.aligned .vCheckboxLabel {
flex: 1 0;
padding: 1px 0 0 5px;
}
.aligned label + p,
.aligned label + div.help,
.aligned label + div.readonly {
padding: 0;
margin-left: 0;
}
.aligned p.file-upload {
font-size: 0.8125rem;
}
span.clearable-file-input {
margin-left: 15px;
}
span.clearable-file-input label {
font-size: 0.8125rem;
padding-bottom: 0;
}
.aligned .timezonewarning {
flex: 1 0 100%;
margin-top: 5px;
}
form .aligned .form-row div.help {
width: 100%;
margin: 5px 0 0;
padding: 0;
}
form .aligned ul,
form .aligned ul.errorlist {
margin-left: 0;
padding-left: 0;
}
form .aligned div.radiolist {
margin-top: 5px;
margin-right: 15px;
margin-bottom: -3px;
}
form .aligned div.radiolist:not(.inline) div + div {
margin-top: 5px;
}
/* Related widget */
.related-widget-wrapper {
width: 100%;
display: flex;
align-items: flex-start;
}
.related-widget-wrapper .selector {
order: 1;
}
.related-widget-wrapper > a {
order: 2;
}
.related-widget-wrapper .radiolist ~ a {
align-self: flex-end;
}
.related-widget-wrapper > select ~ a {
align-self: center;
}
select + .related-widget-wrapper-link,
.related-widget-wrapper-link + .related-widget-wrapper-link {
margin-left: 15px;
}
/* Selector */
.selector {
flex-direction: column;
}
.selector > * {
float: none;
}
.selector-available, .selector-chosen {
margin-bottom: 0;
flex: 1 1 auto;
}
.selector select {
max-height: 96px;
}
.selector ul.selector-chooser {
display: block;
float: none;
width: 52px;
height: 26px;
padding: 0 2px;
margin: 15px auto 20px;
transform: none;
}
.selector ul.selector-chooser li {
float: left;
}
.selector-remove {
background-position: 0 0;
}
.active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -20px;
}
.selector-add {
background-position: 0 -40px;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -60px;
}
/* Inlines */
.inline-group[data-inline-type="stacked"] .inline-related {
border: 1px solid var(--hairline-color);
border-radius: 4px;
margin-top: 15px;
overflow: auto;
}
.inline-group[data-inline-type="stacked"] .inline-related > * {
box-sizing: border-box;
}
.inline-group[data-inline-type="stacked"] .inline-related .module {
padding: 0 10px;
}
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row {
border-top: 1px solid var(--hairline-color);
border-bottom: none;
}
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row:first-child {
border-top: none;
}
.inline-group[data-inline-type="stacked"] .inline-related h3 {
padding: 10px;
border-top-width: 0;
border-bottom-width: 2px;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.inline-group[data-inline-type="stacked"] .inline-related h3 .inline_label {
margin-right: auto;
}
.inline-group[data-inline-type="stacked"] .inline-related h3 span.delete {
float: none;
flex: 1 1 100%;
margin-top: 5px;
}
.inline-group[data-inline-type="stacked"] .aligned .form-row > div:not([class]) {
width: 100%;
}
.inline-group[data-inline-type="stacked"] .aligned label {
width: 100%;
}
.inline-group[data-inline-type="stacked"] div.add-row {
margin-top: 15px;
border: 1px solid var(--hairline-color);
border-radius: 4px;
}
.inline-group div.add-row,
.inline-group .tabular tr.add-row td {
padding: 0;
}
.inline-group div.add-row a,
.inline-group .tabular tr.add-row td a {
display: block;
padding: 8px 10px 8px 26px;
background-position: 8px 9px;
}
/* Submit row */
.submit-row {
padding: 10px;
margin: 0 0 15px;
flex-direction: column;
gap: 8px;
}
.submit-row input, .submit-row input.default, .submit-row a {
text-align: center;
}
.submit-row a.closelink {
padding: 10px 0;
text-align: center;
}
.submit-row a.deletelink {
margin: 0;
}
/* Messages */
ul.messagelist li {
padding-left: 40px;
background-position: 15px 12px;
}
ul.messagelist li.error {
background-position: 15px 12px;
}
ul.messagelist li.warning {
background-position: 15px 14px;
}
/* Paginator */
.paginator .this-page, .paginator a:link, .paginator a:visited {
padding: 4px 10px;
}
/* Login */
body.login {
padding: 0 15px;
}
.login #container {
width: auto;
max-width: 480px;
margin: 50px auto;
}
.login #header,
.login #content {
padding: 15px;
}
.login #content-main {
float: none;
}
.login .form-row {
padding: 0;
}
.login .form-row + .form-row {
margin-top: 15px;
}
.login .form-row label {
margin: 0 0 5px;
line-height: 1.2;
}
.login .submit-row {
padding: 15px 0 0;
}
.login br {
display: none;
}
.login .submit-row input {
margin: 0;
text-transform: uppercase;
}
.errornote {
margin: 0 0 20px;
padding: 8px 12px;
font-size: 0.8125rem;
}
/* Calendar and clock */
.calendarbox, .clockbox {
position: fixed !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%);
margin: 0;
border: none;
overflow: visible;
}
.calendarbox:before, .clockbox:before {
content: '';
position: fixed;
top: 50%;
left: 50%;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.75);
transform: translate(-50%, -50%);
}
.calendarbox > *, .clockbox > * {
position: relative;
z-index: 1;
}
.calendarbox > div:first-child {
z-index: 2;
}
.calendarbox .calendar, .clockbox h2 {
border-radius: 4px 4px 0 0;
overflow: hidden;
}
.calendarbox .calendar-cancel, .clockbox .calendar-cancel {
border-radius: 0 0 4px 4px;
overflow: hidden;
}
.calendar-shortcuts {
padding: 10px 0;
font-size: 0.75rem;
line-height: 0.75rem;
}
.calendar-shortcuts a {
margin: 0 4px;
}
.timelist a {
background: var(--body-bg);
padding: 4px;
}
.calendar-cancel {
padding: 8px 10px;
}
.clockbox h2 {
padding: 8px 15px;
}
.calendar caption {
padding: 10px;
}
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
z-index: 1;
top: 10px;
}
/* History */
table#change-history tbody th, table#change-history tbody td {
font-size: 0.8125rem;
word-break: break-word;
}
table#change-history tbody th {
width: auto;
}
/* Docs */
table.model tbody th, table.model tbody td {
font-size: 0.8125rem;
word-break: break-word;
}
}

View File

@@ -0,0 +1,81 @@
/* TABLETS */
@media (max-width: 1024px) {
[dir="rtl"] .colMS {
margin-right: 0;
}
[dir="rtl"] #user-tools {
text-align: right;
}
[dir="rtl"] #changelist .actions label {
padding-left: 10px;
padding-right: 0;
}
[dir="rtl"] #changelist .actions select {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .change-list .filtered .results,
[dir="rtl"] .change-list .filtered .paginator,
[dir="rtl"] .filtered #toolbar,
[dir="rtl"] .filtered div.xfull,
[dir="rtl"] .filtered .actions,
[dir="rtl"] #changelist-filter {
margin-left: 0;
}
[dir="rtl"] .inline-group ul.tools a.add,
[dir="rtl"] .inline-group div.add-row a,
[dir="rtl"] .inline-group .tabular tr.add-row td a {
padding: 8px 26px 8px 10px;
background-position: calc(100% - 8px) 9px;
}
[dir="rtl"] .related-widget-wrapper-link + .selector {
margin-right: 0;
margin-left: 15px;
}
[dir="rtl"] .selector .selector-filter label {
margin-right: 0;
margin-left: 8px;
}
[dir="rtl"] .object-tools li {
float: right;
}
[dir="rtl"] .object-tools li + li {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .dashboard .module table td a {
padding-left: 0;
padding-right: 16px;
}
}
/* MOBILE */
@media (max-width: 767px) {
[dir="rtl"] .aligned .related-lookup,
[dir="rtl"] .aligned .datetimeshortcuts {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .aligned ul,
[dir="rtl"] form .aligned ul.errorlist {
margin-right: 0;
}
[dir="rtl"] #changelist-filter {
margin-left: 0;
margin-right: 0;
}
}

288
static/admin/css/rtl.css Normal file
View File

@@ -0,0 +1,288 @@
/* GLOBAL */
th {
text-align: right;
}
.module h2, .module caption {
text-align: right;
}
.module ul, .module ol {
margin-left: 0;
margin-right: 1.5em;
}
.viewlink, .addlink, .changelink {
padding-left: 0;
padding-right: 16px;
background-position: 100% 1px;
}
.deletelink {
padding-left: 0;
padding-right: 16px;
background-position: 100% 1px;
}
.object-tools {
float: left;
}
thead th:first-child,
tfoot td:first-child {
border-left: none;
}
/* LAYOUT */
#user-tools {
right: auto;
left: 0;
text-align: left;
}
div.breadcrumbs {
text-align: right;
}
#content-main {
float: right;
}
#content-related {
float: left;
margin-left: -300px;
margin-right: auto;
}
.colMS {
margin-left: 300px;
margin-right: 0;
}
/* SORTABLE TABLES */
table thead th.sorted .sortoptions {
float: left;
}
thead th.sorted .text {
padding-right: 0;
padding-left: 42px;
}
/* dashboard styles */
.dashboard .module table td a {
padding-left: .6em;
padding-right: 16px;
}
/* changelists styles */
.change-list .filtered table {
border-left: none;
border-right: 0px none;
}
#changelist-filter {
border-left: none;
border-right: none;
margin-left: 0;
margin-right: 30px;
}
#changelist-filter li.selected {
border-left: none;
padding-left: 10px;
margin-left: 0;
border-right: 5px solid var(--hairline-color);
padding-right: 10px;
margin-right: -15px;
}
#changelist table tbody td:first-child, #changelist table tbody th:first-child {
border-right: none;
border-left: none;
}
/* FORMS */
.aligned label {
padding: 0 0 3px 1em;
}
.submit-row a.deletelink {
margin-left: 0;
margin-right: auto;
}
.vDateField, .vTimeField {
margin-left: 2px;
}
.aligned .form-row input {
margin-left: 5px;
}
form .aligned ul {
margin-right: 163px;
padding-right: 10px;
margin-left: 0;
padding-left: 0;
}
form ul.inline li {
float: right;
padding-right: 0;
padding-left: 7px;
}
form .aligned p.help,
form .aligned div.help {
margin-right: 160px;
padding-right: 10px;
}
form div.help ul,
form .aligned .checkbox-row + .help,
form .aligned p.date div.help.timezonewarning,
form .aligned p.datetime div.help.timezonewarning,
form .aligned p.time div.help.timezonewarning {
margin-right: 0;
padding-right: 0;
}
form .wide p.help, form .wide div.help {
padding-left: 0;
padding-right: 50px;
}
form .wide p,
form .wide ul.errorlist,
form .wide input + p.help,
form .wide input + div.help {
margin-right: 200px;
margin-left: 0px;
}
.submit-row {
text-align: right;
}
fieldset .fieldBox {
margin-left: 20px;
margin-right: 0;
}
.errorlist li {
background-position: 100% 12px;
padding: 0;
}
.errornote {
background-position: 100% 12px;
padding: 10px 12px;
}
/* WIDGETS */
.calendarnav-previous {
top: 0;
left: auto;
right: 10px;
background: url(../img/calendar-icons.svg) 0 -30px no-repeat;
}
.calendarbox .calendarnav-previous:focus,
.calendarbox .calendarnav-previous:hover {
background-position: 0 -45px;
}
.calendarnav-next {
top: 0;
right: auto;
left: 10px;
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
}
.calendarbox .calendarnav-next:focus,
.calendarbox .calendarnav-next:hover {
background-position: 0 -15px;
}
.calendar caption, .calendarbox h2 {
text-align: center;
}
.selector {
float: right;
}
.selector .selector-filter {
text-align: right;
}
.selector-add {
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -80px;
}
.selector-remove {
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
}
.active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -112px;
}
a.selector-chooseall {
background: url(../img/selector-icons.svg) right -128px no-repeat;
}
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
background-position: 100% -144px;
}
a.selector-clearall {
background: url(../img/selector-icons.svg) 0 -160px no-repeat;
}
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
background-position: 0 -176px;
}
.inline-deletelink {
float: left;
}
form .form-row p.datetime {
overflow: hidden;
}
.related-widget-wrapper {
float: right;
}
/* MISC */
.inline-related h2, .inline-group h2 {
text-align: right
}
.inline-related h3 span.delete {
padding-right: 20px;
padding-left: inherit;
left: 10px;
right: inherit;
float:left;
}
.inline-related h3 span.delete label {
margin-left: inherit;
margin-right: 2px;
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,481 @@
.select2-container {
box-sizing: border-box;
display: inline-block;
margin: 0;
position: relative;
vertical-align: middle; }
.select2-container .select2-selection--single {
box-sizing: border-box;
cursor: pointer;
display: block;
height: 28px;
user-select: none;
-webkit-user-select: none; }
.select2-container .select2-selection--single .select2-selection__rendered {
display: block;
padding-left: 8px;
padding-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
.select2-container .select2-selection--single .select2-selection__clear {
position: relative; }
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
padding-right: 8px;
padding-left: 20px; }
.select2-container .select2-selection--multiple {
box-sizing: border-box;
cursor: pointer;
display: block;
min-height: 32px;
user-select: none;
-webkit-user-select: none; }
.select2-container .select2-selection--multiple .select2-selection__rendered {
display: inline-block;
overflow: hidden;
padding-left: 8px;
text-overflow: ellipsis;
white-space: nowrap; }
.select2-container .select2-search--inline {
float: left; }
.select2-container .select2-search--inline .select2-search__field {
box-sizing: border-box;
border: none;
font-size: 100%;
margin-top: 5px;
padding: 0; }
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none; }
.select2-dropdown {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
box-sizing: border-box;
display: block;
position: absolute;
left: -100000px;
width: 100%;
z-index: 1051; }
.select2-results {
display: block; }
.select2-results__options {
list-style: none;
margin: 0;
padding: 0; }
.select2-results__option {
padding: 6px;
user-select: none;
-webkit-user-select: none; }
.select2-results__option[aria-selected] {
cursor: pointer; }
.select2-container--open .select2-dropdown {
left: 0; }
.select2-container--open .select2-dropdown--above {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--open .select2-dropdown--below {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-search--dropdown {
display: block;
padding: 4px; }
.select2-search--dropdown .select2-search__field {
padding: 4px;
width: 100%;
box-sizing: border-box; }
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
-webkit-appearance: none; }
.select2-search--dropdown.select2-search--hide {
display: none; }
.select2-close-mask {
border: 0;
margin: 0;
padding: 0;
display: block;
position: fixed;
left: 0;
top: 0;
min-height: 100%;
min-width: 100%;
height: auto;
width: auto;
opacity: 0;
z-index: 99;
background-color: #fff;
filter: alpha(opacity=0); }
.select2-hidden-accessible {
border: 0 !important;
clip: rect(0 0 0 0) !important;
-webkit-clip-path: inset(50%) !important;
clip-path: inset(50%) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important; }
.select2-container--default .select2-selection--single {
background-color: #fff;
border: 1px solid #aaa;
border-radius: 4px; }
.select2-container--default .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px; }
.select2-container--default .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold; }
.select2-container--default .select2-selection--single .select2-selection__placeholder {
color: #999; }
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px; }
.select2-container--default .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0; }
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left; }
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto; }
.select2-container--default.select2-container--disabled .select2-selection--single {
background-color: #eee;
cursor: default; }
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none; }
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px; }
.select2-container--default .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text; }
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 5px;
width: 100%; }
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
list-style: none; }
.select2-container--default .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-top: 5px;
margin-right: 10px;
padding: 1px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: #999;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #333; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto; }
.select2-container--default.select2-container--focus .select2-selection--multiple {
border: solid black 1px;
outline: 0; }
.select2-container--default.select2-container--disabled .select2-selection--multiple {
background-color: #eee;
cursor: default; }
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
display: none; }
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--default .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa; }
.select2-container--default .select2-search--inline .select2-search__field {
background: transparent;
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield; }
.select2-container--default .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto; }
.select2-container--default .select2-results__option[role=group] {
padding: 0; }
.select2-container--default .select2-results__option[aria-disabled=true] {
color: #999; }
.select2-container--default .select2-results__option[aria-selected=true] {
background-color: #ddd; }
.select2-container--default .select2-results__option .select2-results__option {
padding-left: 1em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -4em;
padding-left: 5em; }
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -5em;
padding-left: 6em; }
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: #5897fb;
color: white; }
.select2-container--default .select2-results__group {
cursor: default;
display: block;
padding: 6px; }
.select2-container--classic .select2-selection--single {
background-color: #f7f7f7;
border: 1px solid #aaa;
border-radius: 4px;
outline: 0;
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
.select2-container--classic .select2-selection--single:focus {
border: 1px solid #5897fb; }
.select2-container--classic .select2-selection--single .select2-selection__rendered {
color: #444;
line-height: 28px; }
.select2-container--classic .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin-right: 10px; }
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
color: #999; }
.select2-container--classic .select2-selection--single .select2-selection__arrow {
background-color: #ddd;
border: none;
border-left: 1px solid #aaa;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
.select2-container--classic .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0; }
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left; }
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
border: none;
border-right: 1px solid #aaa;
border-radius: 0;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
left: 1px;
right: auto; }
.select2-container--classic.select2-container--open .select2-selection--single {
border: 1px solid #5897fb; }
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
background: transparent;
border: none; }
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px; }
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
.select2-container--classic .select2-selection--multiple {
background-color: white;
border: 1px solid #aaa;
border-radius: 4px;
cursor: text;
outline: 0; }
.select2-container--classic .select2-selection--multiple:focus {
border: 1px solid #5897fb; }
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
list-style: none;
margin: 0;
padding: 0 5px; }
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
display: none; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
background-color: #e4e4e4;
border: 1px solid #aaa;
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
color: #888;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px; }
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
color: #555; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
float: right;
margin-left: 5px;
margin-right: auto; }
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto; }
.select2-container--classic.select2-container--open .select2-selection--multiple {
border: 1px solid #5897fb; }
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0; }
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0; }
.select2-container--classic .select2-search--dropdown .select2-search__field {
border: 1px solid #aaa;
outline: 0; }
.select2-container--classic .select2-search--inline .select2-search__field {
outline: 0;
box-shadow: none; }
.select2-container--classic .select2-dropdown {
background-color: white;
border: 1px solid transparent; }
.select2-container--classic .select2-dropdown--above {
border-bottom: none; }
.select2-container--classic .select2-dropdown--below {
border-top: none; }
.select2-container--classic .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto; }
.select2-container--classic .select2-results__option[role=group] {
padding: 0; }
.select2-container--classic .select2-results__option[aria-disabled=true] {
color: grey; }
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
background-color: #3875d7;
color: white; }
.select2-container--classic .select2-results__group {
cursor: default;
display: block;
padding: 6px; }
.select2-container--classic.select2-container--open .select2-dropdown {
border-color: #5897fb; }

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,603 @@
/* SELECTOR (FILTER INTERFACE) */
.selector {
width: 800px;
float: left;
display: flex;
}
.selector select {
width: 380px;
height: 17.2em;
flex: 1 0 auto;
}
.selector-available, .selector-chosen {
width: 380px;
text-align: center;
margin-bottom: 5px;
display: flex;
flex-direction: column;
}
.selector-available h2, .selector-chosen h2 {
border: 1px solid var(--border-color);
border-radius: 4px 4px 0 0;
}
.selector-chosen .list-footer-display {
border: 1px solid var(--border-color);
border-top: none;
border-radius: 0 0 4px 4px;
margin: 0 0 10px;
padding: 8px;
text-align: center;
background: var(--primary);
color: var(--header-link-color);
cursor: pointer;
}
.selector-chosen .list-footer-display__clear {
color: var(--breadcrumbs-fg);
}
.selector-chosen h2 {
background: var(--primary);
color: var(--header-link-color);
}
.selector .selector-available h2 {
background: var(--darkened-bg);
color: var(--body-quiet-color);
}
.selector .selector-filter {
border: 1px solid var(--border-color);
border-width: 0 1px;
padding: 8px;
color: var(--body-quiet-color);
font-size: 0.625rem;
margin: 0;
text-align: left;
}
.selector .selector-filter label,
.inline-group .aligned .selector .selector-filter label {
float: left;
margin: 7px 0 0;
width: 18px;
height: 18px;
padding: 0;
overflow: hidden;
line-height: 1;
}
.selector .selector-available input,
.selector .selector-chosen input {
width: 320px;
margin-left: 8px;
}
.selector ul.selector-chooser {
align-self: center;
width: 22px;
background-color: var(--selected-bg);
border-radius: 10px;
margin: 0 5px;
padding: 0;
transform: translateY(-17px);
}
.selector-chooser li {
margin: 0;
padding: 3px;
list-style-type: none;
}
.selector select {
padding: 0 10px;
margin: 0 0 10px;
border-radius: 0 0 4px 4px;
}
.selector .selector-chosen--with-filtered select {
margin: 0;
border-radius: 0;
height: 14em;
}
.selector .selector-chosen:not(.selector-chosen--with-filtered) .list-footer-display {
display: none;
}
.selector-add, .selector-remove {
width: 16px;
height: 16px;
display: block;
text-indent: -3000px;
overflow: hidden;
cursor: default;
opacity: 0.55;
}
.active.selector-add, .active.selector-remove {
opacity: 1;
}
.active.selector-add:hover, .active.selector-remove:hover {
cursor: pointer;
}
.selector-add {
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -112px;
}
.selector-remove {
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
}
.active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -80px;
}
a.selector-chooseall, a.selector-clearall {
display: inline-block;
height: 16px;
text-align: left;
margin: 1px auto 3px;
overflow: hidden;
font-weight: bold;
line-height: 16px;
color: var(--body-quiet-color);
text-decoration: none;
opacity: 0.55;
}
a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
color: var(--link-fg);
}
a.active.selector-chooseall, a.active.selector-clearall {
opacity: 1;
}
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
cursor: pointer;
}
a.selector-chooseall {
padding: 0 18px 0 0;
background: url(../img/selector-icons.svg) right -160px no-repeat;
cursor: default;
}
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
background-position: 100% -176px;
}
a.selector-clearall {
padding: 0 0 0 18px;
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
cursor: default;
}
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
background-position: 0 -144px;
}
/* STACKED SELECTORS */
.stacked {
float: left;
width: 490px;
display: block;
}
.stacked select {
width: 480px;
height: 10.1em;
}
.stacked .selector-available, .stacked .selector-chosen {
width: 480px;
}
.stacked .selector-available {
margin-bottom: 0;
}
.stacked .selector-available input {
width: 422px;
}
.stacked ul.selector-chooser {
height: 22px;
width: 50px;
margin: 0 0 10px 40%;
background-color: #eee;
border-radius: 10px;
transform: none;
}
.stacked .selector-chooser li {
float: left;
padding: 3px 3px 3px 5px;
}
.stacked .selector-chooseall, .stacked .selector-clearall {
display: none;
}
.stacked .selector-add {
background: url(../img/selector-icons.svg) 0 -32px no-repeat;
cursor: default;
}
.stacked .active.selector-add {
background-position: 0 -32px;
cursor: pointer;
}
.stacked .active.selector-add:focus, .stacked .active.selector-add:hover {
background-position: 0 -48px;
cursor: pointer;
}
.stacked .selector-remove {
background: url(../img/selector-icons.svg) 0 0 no-repeat;
cursor: default;
}
.stacked .active.selector-remove {
background-position: 0 0px;
cursor: pointer;
}
.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover {
background-position: 0 -16px;
cursor: pointer;
}
.selector .help-icon {
background: url(../img/icon-unknown.svg) 0 0 no-repeat;
display: inline-block;
vertical-align: middle;
margin: -2px 0 0 2px;
width: 13px;
height: 13px;
}
.selector .selector-chosen .help-icon {
background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat;
}
.selector .search-label-icon {
background: url(../img/search.svg) 0 0 no-repeat;
display: inline-block;
height: 1.125rem;
width: 1.125rem;
}
/* DATE AND TIME */
p.datetime {
line-height: 20px;
margin: 0;
padding: 0;
color: var(--body-quiet-color);
font-weight: bold;
}
.datetime span {
white-space: nowrap;
font-weight: normal;
font-size: 0.6875rem;
color: var(--body-quiet-color);
}
.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
margin-left: 5px;
margin-bottom: 4px;
}
table p.datetime {
font-size: 0.6875rem;
margin-left: 0;
padding-left: 0;
}
.datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon {
position: relative;
display: inline-block;
vertical-align: middle;
height: 16px;
width: 16px;
overflow: hidden;
}
.datetimeshortcuts .clock-icon {
background: url(../img/icon-clock.svg) 0 0 no-repeat;
}
.datetimeshortcuts a:focus .clock-icon,
.datetimeshortcuts a:hover .clock-icon {
background-position: 0 -16px;
}
.datetimeshortcuts .date-icon {
background: url(../img/icon-calendar.svg) 0 0 no-repeat;
top: -1px;
}
.datetimeshortcuts a:focus .date-icon,
.datetimeshortcuts a:hover .date-icon {
background-position: 0 -16px;
}
.timezonewarning {
font-size: 0.6875rem;
color: var(--body-quiet-color);
}
/* URL */
p.url {
line-height: 20px;
margin: 0;
padding: 0;
color: var(--body-quiet-color);
font-size: 0.6875rem;
font-weight: bold;
}
.url a {
font-weight: normal;
}
/* FILE UPLOADS */
p.file-upload {
line-height: 20px;
margin: 0;
padding: 0;
color: var(--body-quiet-color);
font-size: 0.6875rem;
font-weight: bold;
}
.file-upload a {
font-weight: normal;
}
.file-upload .deletelink {
margin-left: 5px;
}
span.clearable-file-input label {
color: var(--body-fg);
font-size: 0.6875rem;
display: inline;
float: none;
}
/* CALENDARS & CLOCKS */
.calendarbox, .clockbox {
margin: 5px auto;
font-size: 0.75rem;
width: 19em;
text-align: center;
background: var(--body-bg);
color: var(--body-fg);
border: 1px solid var(--hairline-color);
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
overflow: hidden;
position: relative;
}
.clockbox {
width: auto;
}
.calendar {
margin: 0;
padding: 0;
}
.calendar table {
margin: 0;
padding: 0;
border-collapse: collapse;
background: white;
width: 100%;
}
.calendar caption, .calendarbox h2 {
margin: 0;
text-align: center;
border-top: none;
font-weight: 700;
font-size: 0.75rem;
color: #333;
background: var(--accent);
}
.calendar th {
padding: 8px 5px;
background: var(--darkened-bg);
border-bottom: 1px solid var(--border-color);
font-weight: 400;
font-size: 0.75rem;
text-align: center;
color: var(--body-quiet-color);
}
.calendar td {
font-weight: 400;
font-size: 0.75rem;
text-align: center;
padding: 0;
border-top: 1px solid var(--hairline-color);
border-bottom: none;
}
.calendar td.selected a {
background: var(--primary);
color: var(--button-fg);
}
.calendar td.nonday {
background: var(--darkened-bg);
}
.calendar td.today a {
font-weight: 700;
}
.calendar td a, .timelist a {
display: block;
font-weight: 400;
padding: 6px;
text-decoration: none;
color: var(--body-quiet-color);
}
.calendar td a:focus, .timelist a:focus,
.calendar td a:hover, .timelist a:hover {
background: var(--primary);
color: white;
}
.calendar td a:active, .timelist a:active {
background: var(--header-bg);
color: white;
}
.calendarnav {
font-size: 0.625rem;
text-align: center;
color: #ccc;
margin: 0;
padding: 1px 3px;
}
.calendarnav a:link, #calendarnav a:visited,
#calendarnav a:focus, #calendarnav a:hover {
color: var(--body-quiet-color);
}
.calendar-shortcuts {
background: var(--body-bg);
color: var(--body-quiet-color);
font-size: 0.6875rem;
line-height: 0.6875rem;
border-top: 1px solid var(--hairline-color);
padding: 8px 0;
}
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
display: block;
position: absolute;
top: 8px;
width: 15px;
height: 15px;
text-indent: -9999px;
padding: 0;
}
.calendarnav-previous {
left: 10px;
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
}
.calendarbox .calendarnav-previous:focus,
.calendarbox .calendarnav-previous:hover {
background-position: 0 -15px;
}
.calendarnav-next {
right: 10px;
background: url(../img/calendar-icons.svg) 0 -30px no-repeat;
}
.calendarbox .calendarnav-next:focus,
.calendarbox .calendarnav-next:hover {
background-position: 0 -45px;
}
.calendar-cancel {
margin: 0;
padding: 4px 0;
font-size: 0.75rem;
background: #eee;
border-top: 1px solid var(--border-color);
color: var(--body-fg);
}
.calendar-cancel:focus, .calendar-cancel:hover {
background: #ddd;
}
.calendar-cancel a {
color: black;
display: block;
}
ul.timelist, .timelist li {
list-style-type: none;
margin: 0;
padding: 0;
}
.timelist a {
padding: 2px;
}
/* EDIT INLINE */
.inline-deletelink {
float: right;
text-indent: -9999px;
background: url(../img/inline-delete.svg) 0 0 no-repeat;
width: 16px;
height: 16px;
border: 0px none;
}
.inline-deletelink:focus, .inline-deletelink:hover {
cursor: pointer;
}
/* RELATED WIDGET WRAPPER */
.related-widget-wrapper {
float: left; /* display properly in form rows with multiple fields */
overflow: hidden; /* clear floated contents */
}
.related-widget-wrapper-link {
opacity: 0.3;
}
.related-widget-wrapper-link:link {
opacity: .8;
}
.related-widget-wrapper-link:link:focus,
.related-widget-wrapper-link:link:hover {
opacity: 1;
}
select + .related-widget-wrapper-link,
.related-widget-wrapper-link + .related-widget-wrapper-link {
margin-left: 7px;
}
/* GIS MAPS */
.dj_map {
width: 600px;
height: 400px;
}

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,3 @@
Roboto webfont source: https://www.google.com/fonts/specimen/Roboto
WOFF files extracted using https://github.com/majodev/google-webfonts-helper
Weights used in this project: Light (300), Regular (400), Bold (700)

Binary file not shown.

Binary file not shown.

Binary file not shown.

20
static/admin/img/LICENSE Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 Code Charm Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,7 @@
All icons are taken from Font Awesome (http://fontawesome.io/) project.
The Font Awesome font is licensed under the SIL OFL 1.1:
- https://scripts.sil.org/OFL
SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG
Font-Awesome-SVG-PNG is licensed under the MIT license (see file license
in current folder).

View File

@@ -0,0 +1,14 @@
<svg width="15" height="60" viewBox="0 0 1792 7168" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="previous">
<path d="M1037 1395l102-102q19-19 19-45t-19-45l-307-307 307-307q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-454 454q-19 19-19 45t19 45l454 454q19 19 45 19t45-19zm627-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
<g id="next">
<path d="M845 1395l454-454q19-19 19-45t-19-45l-454-454q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l307 307-307 307q-19 19-19 45t19 45l102 102q19 19 45 19t45-19zm819-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
</g>
</defs>
<use xlink:href="#previous" x="0" y="0" fill="#333333" />
<use xlink:href="#previous" x="0" y="1792" fill="#000000" />
<use xlink:href="#next" x="0" y="3584" fill="#333333" />
<use xlink:href="#next" x="0" y="5376" fill="#000000" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#EBECE6" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9C9C9" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#F1C02A" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9A741" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#70bf2b" d="M1600 796v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z"/>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#efb80b" d="M1024 1375v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13 0-22.5 9.5t-9.5 23.5v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17h-1536q-34 0-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31 47-49t65-18 65 18 47 49z"/>
</svg>

After

Width:  |  Height:  |  Size: 504 B

Some files were not shown because too many files have changed in this diff Show More