├── .gitignore
├── LICENSE
├── README.md
├── alembic.ini
├── config
├── development.yaml
└── production.yaml
├── images
├── dictonary.png
├── dictonary_items.png
├── job_manage.png
├── job_playbook.png
├── login.png
├── menu.png
├── menu_add.png
├── role.png
├── role_add.png
├── user.png
└── user_add.png
├── myapp.py
├── requirements.txt
├── rpyc_scheduler
├── __init__.py
├── config.py
├── job.py
├── jobstore.py
├── models.py
├── scheduler.py
├── server.py
├── tasks.py
└── utils.py
├── server
├── .idea
│ ├── .gitignore
│ ├── deployment.xml
│ ├── inspectionProfiles
│ │ └── profiles_settings.xml
│ ├── misc.xml
│ ├── modules.xml
│ ├── server.iml
│ └── vcs.xml
├── README.md
├── __init__.py
├── alembic.ini
├── alembic
│ ├── README
│ ├── __pycache__
│ │ └── env.cpython-39.pyc
│ ├── env.py
│ ├── script.py.mako
│ └── versions
│ │ ├── 001da528b756_添加数据字典.py
│ │ └── 9edc223ae20a_update数据字典.py
├── common
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-39.pyc
│ │ ├── security.cpython-39.pyc
│ │ └── utils.cpython-39.pyc
│ ├── auth_casbin.py
│ ├── database.py
│ ├── dep.py
│ ├── log.py
│ ├── response_code.py
│ ├── security.py
│ └── utils.py
├── crud
│ ├── __init__.py
│ ├── base.py
│ └── internal
│ │ ├── __init__.py
│ │ ├── dictonary.py
│ │ ├── host.py
│ │ ├── job.py
│ │ ├── menu.py
│ │ ├── playbook.py
│ │ ├── roles.py
│ │ └── user.py
├── data.py
├── db_init.py
├── main.py
├── model.conf
├── models
│ ├── __init__.py
│ └── internal
│ │ ├── __init__.py
│ │ ├── dictonary.py
│ │ ├── host.py
│ │ ├── job.py
│ │ ├── menu.py
│ │ ├── playbook.py
│ │ ├── relationships.py
│ │ ├── role.py
│ │ └── user.py
├── requirements.txt
├── routers
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-39.pyc
│ │ ├── menu.cpython-39.pyc
│ │ ├── roles.cpython-39.pyc
│ │ └── user.cpython-39.pyc
│ └── internal
│ │ ├── __init__.py
│ │ ├── dictonary.py
│ │ ├── host.py
│ │ ├── job.py
│ │ ├── login.py
│ │ ├── menu.py
│ │ ├── playbook.py
│ │ ├── roles.py
│ │ └── user.py
├── settings.py
├── sql
│ └── __pycache__
│ │ ├── __init__.cpython-39.pyc
│ │ ├── crud.cpython-39.pyc
│ │ ├── database.cpython-39.pyc
│ │ ├── models.cpython-39.pyc
│ │ └── schemas.cpython-39.pyc
└── static
│ └── template
│ └── system.xlsx
├── service.sh
├── todo-list.md
└── www
├── .env.development
├── .env.developmentv
├── .env.production
├── .gitignore
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
└── favicon.ico
├── src
├── App.vue
├── api
│ ├── dictonary.js
│ ├── file.js
│ ├── host.js
│ ├── jobs.js
│ ├── menus.js
│ ├── playbook.js
│ ├── roles.js
│ └── users.js
├── assets
│ └── logo.png
├── autoImport.d.ts
├── components.d.ts
├── components
│ ├── AutoDict.vue
│ ├── AutoFormDialog.vue
│ ├── HeaderContent.vue
│ ├── InputPlus.vue
│ ├── MenuList.vue
│ ├── ResetButton.vue
│ └── roles.vue
├── composables
│ ├── useMenu.js
│ ├── usePagination.js
│ └── useTerm.js
├── main.js
├── permission.js
├── router
│ └── index.js
├── stores
│ ├── collapse.js
│ ├── dict.js
│ ├── index.js
│ └── tabs.js
├── utils
│ ├── auth.js
│ ├── common.js
│ ├── request.js
│ └── router.js
└── views
│ ├── DashBoard.vue
│ ├── Home
│ └── index.vue
│ ├── Layout.vue
│ ├── Login
│ └── index.vue
│ ├── PersonalCenter.vue
│ ├── errorPage
│ ├── 401.vue
│ ├── 404.vue
│ └── NotFound.vue
│ ├── host
│ ├── AddGroup.vue
│ ├── HostDialog.vue
│ └── index.vue
│ ├── jobs
│ ├── Execute
│ │ ├── LogDrawer.vue
│ │ └── index.vue
│ ├── JobManage
│ │ ├── AddTargetsDialog.vue
│ │ ├── JobLogs.vue
│ │ ├── LogDetail.vue
│ │ └── index.vue
│ ├── components
│ │ ├── AddJob.vue
│ │ └── AddTargetsDialog.vue
│ └── playbook
│ │ ├── AddPlaybook.vue
│ │ └── index.vue
│ └── system
│ ├── dictonary
│ ├── AddDict.vue
│ ├── AddItem.vue
│ ├── DictItem.vue
│ └── index.vue
│ ├── menus
│ ├── ButtonForm.vue
│ ├── MenuDrawer.vue
│ ├── MenuForm.vue
│ └── index.vue
│ ├── roles
│ ├── RoleDialog.vue
│ └── index.vue
│ ├── setting
│ └── index.vue
│ └── user
│ ├── ResetPasswdDialog.vue
│ ├── UserDrawer.vue
│ └── index.vue
└── vite.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | server/**/__pycache__/*.pyc
2 | server/**/.idea
3 | *.pyc
4 | *.pyc
5 | *.log
6 | *.pyc
7 |
--------------------------------------------------------------------------------
/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | script_location = alembic
6 |
7 | # template used to generate migration files
8 | # file_template = %%(rev)s_%%(slug)s
9 |
10 | # sys.path path, will be prepended to sys.path if present.
11 | # defaults to the current working directory.
12 | prepend_sys_path = .
13 |
14 | # timezone to use when rendering the date within the migration file
15 | # as well as the filename.
16 | # If specified, requires the python-dateutil library that can be
17 | # installed by adding `alembic[tz]` to the pip requirements
18 | # string value is passed to dateutil.tz.gettz()
19 | # leave blank for localtime
20 | # timezone =
21 |
22 | # max length of characters to apply to the
23 | # "slug" field
24 | # truncate_slug_length = 40
25 |
26 | # set to 'true' to run the environment during
27 | # the 'revision' command, regardless of autogenerate
28 | # revision_environment = false
29 |
30 | # set to 'true' to allow .pyc and .pyo files without
31 | # a source .py file to be detected as revisions in the
32 | # versions/ directory
33 | # sourceless = false
34 |
35 | # version location specification; This defaults
36 | # to alembic/versions. When using multiple version
37 | # directories, initial revisions must be specified with --version-path.
38 | # The path separator used here should be the separator specified by "version_path_separator"
39 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
40 |
41 | # version path separator; As mentioned above, this is the character used to split
42 | # version_locations. Valid values are:
43 | #
44 | # version_path_separator = :
45 | # version_path_separator = ;
46 | # version_path_separator = space
47 | version_path_separator = os # default: use os.pathsep
48 |
49 | # the output encoding used when revision files
50 | # are written from script.py.mako
51 | # output_encoding = utf-8
52 |
53 | sqlalchemy.url = driver://user:pass@localhost/dbname
54 |
55 |
56 | [post_write_hooks]
57 | # post_write_hooks defines scripts or Python functions that are run
58 | # on newly generated revision scripts. See the documentation for further
59 | # detail and examples
60 |
61 | # format using "black" - use the console_scripts runner, against the "black" entrypoint
62 | # hooks = black
63 | # black.type = console_scripts
64 | # black.entrypoint = black
65 | # black.options = -l 79 REVISION_SCRIPT_FILENAME
66 |
67 | # Logging configuration
68 | [loggers]
69 | keys = root,sqlalchemy,alembic
70 |
71 | [handlers]
72 | keys = console
73 |
74 | [formatters]
75 | keys = generic
76 |
77 | [logger_root]
78 | level = WARN
79 | handlers = console
80 | qualname =
81 |
82 | [logger_sqlalchemy]
83 | level = WARN
84 | handlers =
85 | qualname = sqlalchemy.engine
86 |
87 | [logger_alembic]
88 | level = INFO
89 | handlers =
90 | qualname = alembic
91 |
92 | [handler_console]
93 | class = StreamHandler
94 | args = (sys.stderr,)
95 | level = NOTSET
96 | formatter = generic
97 |
98 | [formatter_generic]
99 | format = %(levelname)-5.5s [%(name)s] %(message)s
100 | datefmt = %H:%M:%S
101 |
--------------------------------------------------------------------------------
/config/development.yaml:
--------------------------------------------------------------------------------
1 | # 通用配置
2 | common:
3 | env: development
4 | log_level: DEBUG
5 |
6 | # FastAPI 服务器配置
7 | server:
8 | host: 0.0.0.0
9 | port: 8000
10 | debug: true
11 | # token信息
12 | secret_key: "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
13 | algorithm: "HS256"
14 | access_token_expire_minutes: 30
15 | # 连接数据库
16 | database_uri: "mysql+pymysql://root:123456@localhost/devops"
17 | casbin_model_path: "server/model.conf"
18 | # 白名单
19 | no_verify_url:
20 | - "/"
21 | - "/api/login"
22 | redis:
23 | host: "192.168.137.129"
24 | password: "seraphim"
25 | port: 6379
26 | health_check_interval: 30
27 | # 配置连接rpyc信息
28 | rpyc_config:
29 | host: localhost
30 | port: 18861
31 | config:
32 | allow_public_attrs: true
33 | allow_pickle: true
34 | keepalive: true
35 |
36 | # RPyC Scheduler 配置
37 | scheduler:
38 | rpc_port: 18861
39 | apscheduler_job_store: 'mysql+pymysql://root:123456@192.168.137.129/devops'
40 | redis:
41 | host: "192.168.137.129"
42 | password: "seraphim"
43 | port: 6379
44 | health_check_interval: 30
--------------------------------------------------------------------------------
/config/production.yaml:
--------------------------------------------------------------------------------
1 | # 通用配置
2 | common:
3 | env: production
4 | log_level: DEBUG
5 |
6 | # FastAPI 服务器配置
7 | server:
8 | host: 0.0.0.0
9 | port: 8000
10 | debug: true
11 | # token信息
12 | secret_key: "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
13 | algorithm: "HS256"
14 | access_token_expire_minutes: 30
15 | # 连接数据库
16 | database_uri: "mysql+pymysql://root:123456@localhost/devops"
17 | casbin_model_path: "server/model.conf"
18 | # 白名单
19 | no_verify_url:
20 | - "/"
21 | - "/api/login"
22 | redis:
23 | host: "192.168.137.129"
24 | password: "seraphim"
25 | port: 6379
26 | health_check_interval: 30
27 | # 配置连接rpyc信息
28 | rpyc_config:
29 | host: localhost
30 | port: 18861
31 | config:
32 | allow_public_attrs: true
33 | allow_pickle: true
34 | keepalive: true
35 |
36 | # RPyC Scheduler 配置
37 | scheduler:
38 | rpc_port: 18861
39 | apscheduler_job_store: 'mysql+pymysql://root:123456@192.168.137.129/devops'
40 | redis:
41 | host: "192.168.137.129"
42 | password: "seraphim"
43 | port: 6379
44 | health_check_interval: 30
--------------------------------------------------------------------------------
/images/dictonary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/images/dictonary.png
--------------------------------------------------------------------------------
/images/dictonary_items.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/images/dictonary_items.png
--------------------------------------------------------------------------------
/images/job_manage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/images/job_manage.png
--------------------------------------------------------------------------------
/images/job_playbook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/images/job_playbook.png
--------------------------------------------------------------------------------
/images/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/images/login.png
--------------------------------------------------------------------------------
/images/menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/images/menu.png
--------------------------------------------------------------------------------
/images/menu_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/images/menu_add.png
--------------------------------------------------------------------------------
/images/role.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/images/role.png
--------------------------------------------------------------------------------
/images/role_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/images/role_add.png
--------------------------------------------------------------------------------
/images/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/images/user.png
--------------------------------------------------------------------------------
/images/user_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/images/user_add.png
--------------------------------------------------------------------------------
/myapp.py:
--------------------------------------------------------------------------------
1 | import uvicorn
2 | from server.main import app
3 |
4 | if __name__ == '__main__':
5 | uvicorn.run(app, host="0.0.0.0", port=8000)
6 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/requirements.txt
--------------------------------------------------------------------------------
/rpyc_scheduler/__init__.py:
--------------------------------------------------------------------------------
1 | from .server import main
--------------------------------------------------------------------------------
/rpyc_scheduler/config.py:
--------------------------------------------------------------------------------
1 | import yaml
2 | import os
3 | from pathlib import Path
4 |
5 | # 获取环境变量,默认为 development
6 | env = os.getenv('ENV', 'development')
7 | config_path = Path(__file__).parent.parent / 'config' / f'{env}.yaml'
8 |
9 | # 初始化配置变量
10 | rpc_config = {}
11 |
12 | # 读取配置文件
13 | def load_config():
14 | global rpc_config
15 | with open(config_path, 'r', encoding='utf-8') as f:
16 | config = yaml.safe_load(f)
17 | rpc_config.update(config['scheduler'])
18 |
19 | # 加载配置
20 | load_config()
21 |
--------------------------------------------------------------------------------
/rpyc_scheduler/job.py:
--------------------------------------------------------------------------------
1 | from apscheduler.jobstores.base import BaseJobStore, JobLookupError, ConflictingIdError
2 | from apscheduler.util import maybe_ref, datetime_to_utc_timestamp, utc_timestamp_to_datetime
3 | from apscheduler.job import Job
4 | try:
5 | import cPickle as pickle
6 | except ImportError: # pragma: nocover
7 | import pickle
8 |
9 | try:
10 | from sqlalchemy import (
11 | create_engine, Table, Column, MetaData, Unicode, Float, LargeBinary, select, and_)
12 | from sqlalchemy.exc import IntegrityError
13 | from sqlalchemy.sql.expression import null
14 | except ImportError: # pragma: nocover
15 | raise ImportError('SQLAlchemyJobStore requires SQLAlchemy installed')
16 |
17 |
18 | class MyJobStore(BaseJobStore):
19 | """
20 | 参照apscheduler自带的SQLAlchemyJobStore,重写JobStore
21 | 1. 添加额外的字段
22 | 2. 重写部分方法,实现Job的保持
23 | """
24 |
25 | def __init__(self, url=None, engine=None, tablename='apscheduler_jobs', metadata=None,
26 | pickle_protocol=pickle.HIGHEST_PROTOCOL, tableschema=None, engine_options=None):
27 | super().__init__(url=None, engine=None, tablename='apscheduler_jobs', metadata=None,
28 | pickle_protocol=pickle.HIGHEST_PROTOCOL, tableschema=None, engine_options=None)
29 | self.jobs_t = Table(
30 | tablename, metadata,
31 | Column('id', Unicode(191), primary_key=True),
32 | Column('next_run_time', Float(25), index=True),
33 | Column('job_state', LargeBinary, nullable=False),
34 | schema=tableschema
35 | )
36 |
37 | def start(self, scheduler, alias):
38 | super().start(scheduler, alias)
39 |
--------------------------------------------------------------------------------
/rpyc_scheduler/jobstore.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from loguru import logger
3 | from apscheduler.util import maybe_ref
4 | from apscheduler.job import Job
5 | from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
6 | from sqlalchemy import (
7 | create_engine, Table, Column, MetaData, Unicode, Float, LargeBinary, select, and_, text, Integer)
8 |
9 | try:
10 | import cPickle as pickle
11 | except ImportError: # pragma: nocover
12 | import pickle
13 |
14 |
15 | class CustomJobStore(SQLAlchemyJobStore):
16 |
17 | def get_multi_jobs(self, uid=None):
18 | """
19 | 通过job_id查询多个job
20 | """
21 | jobs = []
22 | selectable = select(self.jobs_t.c.id, self.jobs_t.c.job_state). \
23 | order_by(self.jobs_t.c.next_run_time)
24 | selectable = selectable.where(self.jobs_t.c.id == uid) if uid else selectable
25 | logger.debug(selectable)
26 | failed_job_ids = set()
27 | with self.engine.begin() as connection:
28 | for row in connection.execute(selectable):
29 | try:
30 | jobs.append(self._reconstitute_job(row.job_state))
31 | except BaseException:
32 | self._logger.exception('Unable to restore job "%s" -- removing it', row.id)
33 | failed_job_ids.add(row.id)
34 |
35 | # Remove all the jobs we failed to restore
36 | if failed_job_ids:
37 | delete = self.jobs_t.delete().where(self.jobs_t.c.id.in_(failed_job_ids))
38 | connection.execute(delete)
39 | logger.debug(jobs)
40 | return jobs
41 |
42 | def get_user_jobs(self, uid, job_name, jobstore=None) -> list[Job]:
43 | """
44 | 通过uid和job_name,获取用户的jobs列表
45 | :param uid:用户ID
46 | :param job_name:指定匹配job name
47 | :param jobstore: None
48 | """
49 | user_jobs: List[Job] = []
50 | jobs = self.get_multi_jobs(uid)
51 | logger.debug(jobs)
52 | for job in jobs:
53 | if job_name is None:
54 | user_jobs.append(job)
55 | elif (job.name.find(job_name)) >= 0:
56 | user_jobs.append(job)
57 | logger.debug(user_jobs)
58 | return user_jobs
59 |
--------------------------------------------------------------------------------
/rpyc_scheduler/models.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from datetime import datetime
3 | from sqlmodel import SQLModel, Field, Column, Integer, create_engine, Session
4 | from sqlalchemy.dialects import mysql
5 | from .config import rpc_config
6 | from sqlmodel import SQLModel
7 | from typing import Union
8 |
9 | engine = create_engine(str(rpc_config['apscheduler_job_store']), pool_size=5, max_overflow=10, pool_timeout=30,
10 | pool_pre_ping=True)
11 |
12 |
13 | # SQLModel.metadata.create_all(engine)
14 |
15 | class InventoryHost(SQLModel):
16 | """
17 | 主机资产涉及的参数
18 | """
19 | name: str
20 | ansible_host: str
21 | ansible_port: int = 22
22 | ansible_user: str
23 | ansible_password: Union[str, None] = None
24 | ansible_ssh_private_key: Union[str, None] = None
25 |
26 |
27 | class AnsibleInventory(SQLModel):
28 | pass
29 |
--------------------------------------------------------------------------------
/rpyc_scheduler/scheduler.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | from loguru import logger
3 | from datetime import datetime, timedelta
4 | import six
5 | import warnings
6 | from apscheduler.schedulers.background import BackgroundScheduler
7 |
8 | try:
9 | from collections.abc import MutableMapping
10 | except ImportError:
11 | from collections import MutableMapping
12 |
13 | STATE_STOPPED = 0
14 | #: constant indicating a scheduler's running state (started and processing jobs)
15 | STATE_RUNNING = 1
16 | #: constant indicating a scheduler's paused state (started but not processing jobs)
17 | STATE_PAUSED = 2
18 |
19 |
20 | class CustomBackgroundScheduler(BackgroundScheduler):
21 |
22 | def get_user_jobs(self, uid, job_name, jobstore=None, pending=None):
23 | """
24 | 分页搜索jobs
25 |
26 | :param str|unicode jobstore: alias of the job store
27 | :param bool pending: **DEPRECATED**
28 | :rtype: list[Job]
29 |
30 | """
31 | if pending is not None:
32 | warnings.warn('The "pending" option is deprecated -- get_jobs() always returns '
33 | 'scheduled jobs if the scheduler has been started and pending jobs '
34 | 'otherwise', DeprecationWarning)
35 |
36 | with self._jobstores_lock:
37 | jobs = []
38 | if self.state == STATE_STOPPED:
39 | for job, alias, replace_existing in self._pending_jobs:
40 | if jobstore is None or alias == jobstore:
41 | jobs.append(job)
42 | else:
43 | for alias, store in six.iteritems(self._jobstores):
44 | if jobstore is None or alias == jobstore:
45 | jobs.extend(store.get_user_jobs(uid, job_name, jobstore))
46 | logger.debug(jobs)
47 | return jobs
48 |
49 | def get_jobs(self, jobstore=None, pending=None):
50 | """
51 | 分页搜索jobs
52 |
53 | :param str|unicode jobstore: alias of the job store
54 | :param bool pending: **DEPRECATED**
55 | :rtype: list[Job]
56 |
57 | """
58 | if pending is not None:
59 | warnings.warn('The "pending" option is deprecated -- get_jobs() always returns '
60 | 'scheduled jobs if the scheduler has been started and pending jobs '
61 | 'otherwise', DeprecationWarning)
62 |
63 | with self._jobstores_lock:
64 | jobs = []
65 | if self.state == STATE_STOPPED:
66 | for job, alias, replace_existing in self._pending_jobs:
67 | if jobstore is None or alias == jobstore:
68 | jobs.append(job)
69 | else:
70 | for alias, store in six.iteritems(self._jobstores):
71 | if jobstore is None or alias == jobstore:
72 | jobs.extend(store.get_all_jobs())
73 | return jobs
74 |
--------------------------------------------------------------------------------
/rpyc_scheduler/utils.py:
--------------------------------------------------------------------------------
1 | import redis
2 | import os
3 | from loguru import logger
4 | from pathlib import Path
5 | from typing import Dict, Any, List
6 | from .models import InventoryHost
7 |
8 |
9 | class Channel:
10 | def __init__(self, redis_config, job_id):
11 | self.conn = redis.Redis(**redis_config, decode_responses=True)
12 | self.job_id = job_id
13 | self.task_key = f"tasks:{self.job_id}"
14 | self._expire = 15
15 |
16 | @property
17 | def msg(self, ):
18 | return self.conn.xrange(self.task_key, '-', '+')
19 |
20 | @property
21 | def expire(self, ) -> int:
22 | return self._expire
23 |
24 | @expire.setter
25 | def expire(self, value: int):
26 | self._expire = value
27 |
28 | def send(self, msg: Dict[Any, Any]):
29 | self.conn.xadd(self.task_key, msg)
30 |
31 | def delete(self, ):
32 | self.conn.delete(self.task_key)
33 |
34 | def __enter__(self):
35 | return self
36 |
37 | def __exit__(self, exc_type, exc_val, exc_tb):
38 | self.conn.expire(self.task_key, self._expire)
39 | self.close()
40 |
41 | def close(self, ):
42 | self.conn.close()
43 |
44 |
45 | def hosts_to_inventory(hosts: List[InventoryHost], private_data_dir: Path) -> dict:
46 | """
47 | 转换hosts为inventory格式的数据
48 | :params hosts:
49 | :params private_data_dir:ansible-runner的环境目录,其中保存runner执行过程的所有数据
50 | """
51 | inventory = {}
52 | logger.debug(hosts)
53 | for host in hosts:
54 | inventory[host.name] = {
55 | "ansible_host": host.ansible_host,
56 | "ansible_port": host.ansible_port,
57 | "ansible_user": host.ansible_user,
58 | }
59 | if host.ansible_password:
60 | inventory[host.name]["ansible_password"] = host.ansible_password
61 | if host.ansible_ssh_private_key:
62 | # 私钥保存到本地文件,并指向对应路径
63 | private_key_file = private_data_dir / f"{host.ansible_host}"
64 | private_key_file.write_text(host.ansible_ssh_private_key)
65 | os.chmod(str(private_key_file), 0o600)
66 | inventory[host.name]["ansible_ssh_private_key_file"] = str(private_key_file)
67 | return {'all': {'hosts': inventory}}
68 |
--------------------------------------------------------------------------------
/server/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # 基于编辑器的 HTTP 客户端请求
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/server/.idea/deployment.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/server/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/server/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/server/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/server/.idea/server.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/server/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | 服务启动
2 |
3 | uvicorn server.main:app --reload
4 |
5 |
6 | ## 知识点介绍
7 | ### 带参数分页查询
8 | #### Pagination定义
9 | ```python
10 | from typing import Optional, Generic, TypeVar
11 | from pydantic import BaseModel
12 |
13 | T = TypeVar('T')
14 |
15 |
16 | class Pagination(BaseModel, Generic[T]):
17 | search: T
18 | page: Optional[int] = 1
19 | page_size: Optional[int] = 10
20 | model: Optional[str] = 'asc'
21 | ```
22 | #### ApiSearch定义
23 | ```python
24 | from typing import Dict
25 | from ...models.internal.api import ApiBase
26 |
27 |
28 | class ApiSearch(ApiBase):
29 | type: Dict[str, str]
30 | ```
31 | #### ApiBase为数据库字段的定义
32 | ```python
33 | class ApiBase(SQLModel):
34 | tags: str
35 | path: str
36 | method: str
37 | summary: str
38 | deprecated: int = Field(default=0)
39 | ```
40 | #### 解析分页查询请求
41 | ```python
42 | #引入Pagination
43 | from ...schemas.internal.pagination import Pagination
44 |
45 | #定义POST请求,并解析,其中ApiSearch为定义的Pagination.search字段解析
46 | @router.post('/sysapis', summary='获取API列表')
47 | async def get_sysapis(search: Pagination[ApiSearch],
48 | session: Session = Depends(get_session)):
49 | print(search)
50 | total = crud.internal.api.search_total(session, search.search)
51 | print(total)
52 | sys_apis = crud.internal.api.search(session, search)
53 | return {
54 | 'total': total,
55 | 'data': sys_apis
56 | }
57 | ```
58 |
--------------------------------------------------------------------------------
/server/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/server/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | script_location = alembic
6 |
7 | # template used to generate migration files
8 | # file_template = %%(rev)s_%%(slug)s
9 |
10 | # sys.path path, will be prepended to sys.path if present.
11 | # defaults to the current working directory.
12 | prepend_sys_path = .
13 |
14 | # timezone to use when rendering the date within the migration file
15 | # as well as the filename.
16 | # If specified, requires the python-dateutil library that can be
17 | # installed by adding `alembic[tz]` to the pip requirements
18 | # string value is passed to dateutil.tz.gettz()
19 | # leave blank for localtime
20 | # timezone =
21 |
22 | # max length of characters to apply to the
23 | # "slug" field
24 | # truncate_slug_length = 40
25 |
26 | # set to 'true' to run the environment during
27 | # the 'revision' command, regardless of autogenerate
28 | # revision_environment = false
29 |
30 | # set to 'true' to allow .pyc and .pyo files without
31 | # a source .py file to be detected as revisions in the
32 | # versions/ directory
33 | # sourceless = false
34 |
35 | # version location specification; This defaults
36 | # to alembic/versions. When using multiple version
37 | # directories, initial revisions must be specified with --version-path.
38 | # The path separator used here should be the separator specified by "version_path_separator"
39 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
40 |
41 | # version path separator; As mentioned above, this is the character used to split
42 | # version_locations. Valid values are:
43 | #
44 | # version_path_separator = :
45 | # version_path_separator = ;
46 | # version_path_separator = space
47 | version_path_separator = os # default: use os.pathsep
48 |
49 | # the output encoding used when revision files
50 | # are written from script.py.mako
51 | # output_encoding = utf-8
52 |
53 | sqlalchemy.url = mysql+pymysql://root:123456@192.168.137.129/devops
54 |
55 |
56 | [post_write_hooks]
57 | # post_write_hooks defines scripts or Python functions that are run
58 | # on newly generated revision scripts. See the documentation for further
59 | # detail and examples
60 |
61 | # format using "black" - use the console_scripts runner, against the "black" entrypoint
62 | # hooks = black
63 | # black.type = console_scripts
64 | # black.entrypoint = black
65 | # black.options = -l 79 REVISION_SCRIPT_FILENAME
66 |
67 | # Logging configuration
68 | [loggers]
69 | keys = root,sqlalchemy,alembic
70 |
71 | [handlers]
72 | keys = console
73 |
74 | [formatters]
75 | keys = generic
76 |
77 | [logger_root]
78 | level = WARN
79 | handlers = console
80 | qualname =
81 |
82 | [logger_sqlalchemy]
83 | level = WARN
84 | handlers =
85 | qualname = sqlalchemy.engine
86 |
87 | [logger_alembic]
88 | level = INFO
89 | handlers =
90 | qualname = alembic
91 |
92 | [handler_console]
93 | class = StreamHandler
94 | args = (sys.stderr,)
95 | level = NOTSET
96 | formatter = generic
97 |
98 | [formatter_generic]
99 | format = %(levelname)-5.5s [%(name)s] %(message)s
100 | datefmt = %H:%M:%S
101 |
--------------------------------------------------------------------------------
/server/alembic/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/server/alembic/__pycache__/env.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/alembic/__pycache__/env.cpython-39.pyc
--------------------------------------------------------------------------------
/server/alembic/env.py:
--------------------------------------------------------------------------------
1 | from logging.config import fileConfig
2 |
3 | from sqlalchemy import engine_from_config
4 | from sqlalchemy import pool
5 | from sqlmodel import SQLModel
6 |
7 | from alembic import context
8 |
9 | # this is the Alembic Config object, which provides
10 | # access to the values within the .ini file in use.
11 | config = context.config
12 |
13 | # Interpret the config file for Python logging.
14 | # This line sets up loggers basically.
15 | if config.config_file_name is not None:
16 | fileConfig(config.config_file_name)
17 |
18 | # add your model's MetaData object here
19 | # for 'autogenerate' support
20 | # from myapp import mymodel
21 | # target_metadata = mymodel.Base.metadata
22 |
23 | from models import *
24 | from models.internal import *
25 |
26 | target_metadata = SQLModel.metadata
27 |
28 |
29 | def include_name(name, type_, parent_names):
30 | """
31 | 排除某些表
32 | """
33 | exclude_tables = ['apscheduler_jobs', 'casbin_rule']
34 | if name in exclude_tables and type_ == 'table':
35 | return False
36 | else:
37 | return True
38 |
39 |
40 | # other values from the config, defined by the needs of env.py,
41 | # can be acquired:
42 | # my_important_option = config.get_main_option("my_important_option")
43 | # ... etc.
44 |
45 |
46 | def run_migrations_offline() -> None:
47 | """Run migrations in 'offline' mode.
48 |
49 | This configures the context with just a URL
50 | and not an Engine, though an Engine is acceptable
51 | here as well. By skipping the Engine creation
52 | we don't even need a DBAPI to be available.
53 |
54 | Calls to context.execute() here emit the given string to the
55 | script output.
56 |
57 | """
58 | url = config.get_main_option("sqlalchemy.url")
59 | context.configure(
60 | url=url,
61 | target_metadata=target_metadata,
62 | literal_binds=True,
63 | dialect_opts={"paramstyle": "named"},
64 | include_name=include_name
65 | )
66 |
67 | with context.begin_transaction():
68 | context.run_migrations()
69 |
70 |
71 | def run_migrations_online() -> None:
72 | """Run migrations in 'online' mode.
73 |
74 | In this scenario we need to create an Engine
75 | and associate a connection with the context.
76 |
77 | """
78 | connectable = engine_from_config(
79 | config.get_section(config.config_ini_section),
80 | prefix="sqlalchemy.",
81 | poolclass=pool.NullPool,
82 | )
83 |
84 | with connectable.connect() as connection:
85 | context.configure(
86 | connection=connection, target_metadata=target_metadata, compare_type=True, include_name=include_name
87 | )
88 |
89 | with context.begin_transaction():
90 | context.run_migrations()
91 |
92 |
93 | if context.is_offline_mode():
94 | run_migrations_offline()
95 | else:
96 | run_migrations_online()
97 |
--------------------------------------------------------------------------------
/server/alembic/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | import sqlmodel
11 | ${imports if imports else ""}
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = ${repr(up_revision)}
15 | down_revision = ${repr(down_revision)}
16 | branch_labels = ${repr(branch_labels)}
17 | depends_on = ${repr(depends_on)}
18 |
19 |
20 | def upgrade() -> None:
21 | ${upgrades if upgrades else "pass"}
22 |
23 |
24 | def downgrade() -> None:
25 | ${downgrades if downgrades else "pass"}
26 |
--------------------------------------------------------------------------------
/server/alembic/versions/001da528b756_添加数据字典.py:
--------------------------------------------------------------------------------
1 | """添加数据字典
2 |
3 | Revision ID: 001da528b756
4 | Revises:
5 | Create Date: 2022-11-08 10:11:55.957440
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | import sqlmodel
11 | from sqlalchemy.dialects import mysql
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = '001da528b756'
15 | down_revision = None
16 | branch_labels = None
17 | depends_on = None
18 |
19 |
20 | def upgrade() -> None:
21 | # ### commands auto generated by Alembic - please adjust! ###
22 | op.create_table('data_dict',
23 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
24 | sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False, comment='字典名称'),
25 | sa.Column('code', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False, comment='字典编号'),
26 | sa.Column('desc', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True, comment='描述'),
27 | sa.PrimaryKeyConstraint('id')
28 | )
29 | op.create_table('dict_item',
30 | sa.Column('enable', sa.Boolean(), nullable=True, comment='是否启用'),
31 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
32 | sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False, comment='名称'),
33 | sa.Column('data', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False, comment='数据值'),
34 | sa.Column('desc', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True, comment='描述'),
35 | sa.Column('sort', sa.Integer(), nullable=True, comment='排序值,越小越靠前'),
36 | sa.Column('dict_id', sa.Integer(), nullable=True),
37 | sa.ForeignKeyConstraint(['dict_id'], ['data_dict.id'], ),
38 | sa.PrimaryKeyConstraint('id')
39 | )
40 | # ### end Alembic commands ###
41 |
42 |
43 | def downgrade() -> None:
44 | # ### commands auto generated by Alembic - please adjust! ###
45 | op.drop_table('dict_item')
46 | op.drop_table('data_dict')
47 | # ### end Alembic commands ###
48 |
--------------------------------------------------------------------------------
/server/alembic/versions/9edc223ae20a_update数据字典.py:
--------------------------------------------------------------------------------
1 | """update数据字典
2 |
3 | Revision ID: 9edc223ae20a
4 | Revises: 001da528b756
5 | Create Date: 2022-11-11 10:57:04.750421
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | import sqlmodel
11 | from sqlalchemy.dialects import mysql
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = '9edc223ae20a'
15 | down_revision = '001da528b756'
16 | branch_labels = None
17 | depends_on = None
18 |
19 |
20 | def upgrade() -> None:
21 | # ### commands auto generated by Alembic - please adjust! ###
22 | op.add_column('dict_item', sa.Column('label', sqlmodel.sql.sqltypes.AutoString(length=50), nullable=False, comment='名称'))
23 | op.add_column('dict_item', sa.Column('value', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False, comment='数据值'))
24 | op.drop_column('dict_item', 'data')
25 | op.drop_column('dict_item', 'name')
26 | op.alter_column('menu', 'icon',
27 | existing_type=mysql.VARCHAR(collation='utf8mb4_general_ci', length=50),
28 | nullable=True,
29 | existing_comment='Icon图标')
30 | # ### end Alembic commands ###
31 |
32 |
33 | def downgrade() -> None:
34 | # ### commands auto generated by Alembic - please adjust! ###
35 | op.alter_column('menu', 'icon',
36 | existing_type=mysql.VARCHAR(collation='utf8mb4_general_ci', length=50),
37 | nullable=False,
38 | existing_comment='Icon图标')
39 | op.add_column('dict_item', sa.Column('name', mysql.VARCHAR(collation='utf8mb4_general_ci', length=50), nullable=False, comment='名称'))
40 | op.add_column('dict_item', sa.Column('data', mysql.VARCHAR(collation='utf8mb4_general_ci', length=100), nullable=False, comment='数据值'))
41 | op.drop_column('dict_item', 'value')
42 | op.drop_column('dict_item', 'label')
43 | # ### end Alembic commands ###
44 |
--------------------------------------------------------------------------------
/server/common/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/common/__init__.py
--------------------------------------------------------------------------------
/server/common/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/common/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/server/common/__pycache__/security.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/common/__pycache__/security.cpython-39.pyc
--------------------------------------------------------------------------------
/server/common/__pycache__/utils.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/common/__pycache__/utils.cpython-39.pyc
--------------------------------------------------------------------------------
/server/common/auth_casbin.py:
--------------------------------------------------------------------------------
1 | from loguru import logger
2 | from fastapi import Request, HTTPException
3 | from ..settings import casbin_enforcer
4 |
5 |
6 | class Authority:
7 | def __init__(self, policy: str):
8 | self.policy = policy
9 |
10 | def __call__(self, request: Request):
11 | if request.state.uid == 1:
12 | # uid为1的都跳过,为admin用户
13 | return True
14 | model, act = self.policy.split(':')
15 | if not casbin_enforcer.enforce(f'uid_{request.state.uid}', model, act):
16 | logger.warning(f'uid_{request.state.uid} {model} {act} 没有权限')
17 | raise HTTPException(status_code=403, detail="没有权限")
18 |
--------------------------------------------------------------------------------
/server/common/database.py:
--------------------------------------------------------------------------------
1 | import redis.asyncio as redis
2 | import rpyc
3 | from sqlmodel import create_engine, SQLModel, Session, select
4 | from server.settings import settings, engine
5 |
6 |
7 | def init_db():
8 | """
9 | 数据库初始化
10 | :return:
11 | """
12 | SQLModel.metadata.create_all(engine)
13 |
14 |
15 | # 数据库的dependency,用于每次请求都需要创建db连接时使用
16 | def get_session():
17 | with Session(engine) as session:
18 | yield session
19 |
20 |
21 | def get_rpyc():
22 | with rpyc.connect(**settings['rpyc_config']) as conn:
23 | yield conn
24 |
25 |
26 | def get_or_create(session: Session, model, **kwargs):
27 | """
28 | 检查表中是否存在对象,如果不存在就创建
29 | :param session:
30 | :param model: 表模型
31 | :param kwargs: 表模型参数
32 | :return:
33 | """
34 | instance = session.query(model).filter_by(**kwargs).first()
35 | if instance:
36 | return instance
37 | else:
38 | instance = model(**kwargs)
39 | session.add(instance)
40 | session.commit()
41 | return instance
42 |
43 |
44 | async def get_redis():
45 | redis_conn = redis.Redis(**settings['redis'])
46 | try:
47 | yield redis_conn
48 | finally:
49 | await redis_conn.close()
50 |
--------------------------------------------------------------------------------
/server/common/dep.py:
--------------------------------------------------------------------------------
1 | from fastapi import Request
2 |
3 |
4 | async def get_uid(request: Request) -> int:
5 | """
6 | 从request头部中获取uid信息
7 | """
8 | return request.state.uid
9 |
--------------------------------------------------------------------------------
/server/common/log.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from loguru import logger
3 |
4 |
5 | # 参考自:https://gist.github.com/nkhitrov/a3e31cfcc1b19cba8e1b626276148c49
6 |
7 |
8 | class InterceptHandler(logging.Handler):
9 | def emit(self, record):
10 | # Get corresponding Loguru level if it exists
11 | try:
12 | level = logger.level(record.levelname).name
13 | except ValueError:
14 | level = record.levelno
15 |
16 | # Find caller from where originated the logged message
17 | frame, depth = logging.currentframe(), 2
18 | while frame.f_code.co_filename == logging.__file__:
19 | frame = frame.f_back
20 | depth += 1
21 |
22 | logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
23 |
24 |
25 | def init_logging():
26 | # 获取所有uvicorn的日志设定,并重置
27 | loggers = (
28 | logging.getLogger(name)
29 | for name in logging.root.manager.loggerDict
30 | if name.startswith("uvicorn.")
31 | )
32 |
33 | for uvicorn_logger in loggers:
34 | # 为了防止日志重复输出
35 | uvicorn_logger.propagate = False
36 | uvicorn_logger.handlers = [InterceptHandler()]
37 |
--------------------------------------------------------------------------------
/server/common/response_code.py:
--------------------------------------------------------------------------------
1 | from typing import Generic, TypeVar, List, Union
2 | from pydantic import Field
3 | from pydantic import BaseModel
4 |
5 | T = TypeVar("T")
6 | DATA = TypeVar("DATA")
7 |
8 |
9 | class ApiResponse(BaseModel, Generic[T]):
10 | """
11 | 自定义返回模型
12 | """
13 | code: int = Field(default=200, description="返回码")
14 | message: str = Field(default="success", description="消息内容")
15 | data: Union[T, None] = None
16 |
17 |
18 | class SearchResponse(BaseModel, Generic[DATA]):
19 | total: int
20 | data: List[DATA] = []
21 |
--------------------------------------------------------------------------------
/server/common/security.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from loguru import logger
4 | from datetime import datetime, timedelta
5 | from fastapi import Request, WebSocket, HTTPException, status
6 | from fastapi.security import OAuth2PasswordBearer
7 | from fastapi.security.utils import get_authorization_scheme_param
8 | from jose import JWTError, jwt
9 | from passlib.context import CryptContext
10 | from starlette.websockets import WebSocket
11 |
12 | from ..settings import settings
13 |
14 | # to get a string like this run:
15 | # openssl rand -hex 32
16 |
17 |
18 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
19 |
20 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
21 |
22 |
23 | def auth_check(request: Request = None, ws: WebSocket = None):
24 | """
25 | 检查是否有token信息,并在request.state中添加uid值
26 | :param request:
27 | :param ws:
28 | :return:
29 | """
30 | # websocket不需要验证
31 | if ws:
32 | return None
33 | logger.info(f'request url:{request.url} method:{request.method}')
34 | for url in settings['no_verify_url']:
35 | if url == request.url.path.lower():
36 | logger.debug(f"{request.url.path} 在白名单中,不需要权限验证")
37 | return True
38 | authorization: str = request.headers.get("Authorization")
39 | schema, param = get_authorization_scheme_param(authorization)
40 | if not authorization or schema.lower() != "bearer":
41 | raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated")
42 |
43 | try:
44 | playload = jwt.decode(param, settings['secret_key'], settings['algorithm'])
45 | except jwt.ExpiredSignatureError as e:
46 | raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
47 | except JWTError as e:
48 | raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
49 |
50 | uid = playload.get('uid')
51 | # 在Request对象中设置用户对象,这样在其他地方就能通过request.state.uid获取当前用户id了
52 | request.state.uid = uid
53 |
54 |
55 | def create_access_token(data):
56 | """
57 | 生成token
58 | :param data:
59 | :return:
60 | """
61 | expires_delta = timedelta(minutes=settings['access_token_expire_minutes'])
62 | to_encode = data.copy()
63 | if expires_delta:
64 | expire = datetime.utcnow() + expires_delta
65 | else:
66 | expire = datetime.utcnow() + timedelta(minutes=15)
67 | to_encode.update({"exp": expire})
68 | encoded_jwt = jwt.encode(to_encode, settings['secret_key'], algorithm=settings['algorithm'])
69 | return encoded_jwt
70 |
--------------------------------------------------------------------------------
/server/crud/__init__.py:
--------------------------------------------------------------------------------
1 | from .internal.menu import menu
2 |
--------------------------------------------------------------------------------
/server/crud/internal/__init__.py:
--------------------------------------------------------------------------------
1 | from .user import user
2 | from .roles import role
3 | from .menu import menu
4 | from .dictonary import data_dict, dict_item
5 | from .host import host, group
6 | from .job import job_logs
7 | from .playbook import playbook
8 |
--------------------------------------------------------------------------------
/server/crud/internal/dictonary.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List, Dict
2 | from loguru import logger
3 | from sqlmodel import select, Session, desc, delete
4 | from ...models.internal.dictonary import DataDict, DictItem, DictItemSearch
5 | from ..base import CRUDBase
6 | from ...models.internal import Pagination
7 |
8 |
9 | class CRUDDict(CRUDBase[DataDict]):
10 | pass
11 |
12 |
13 | class CRUDItem(CRUDBase[DictItem]):
14 | def get_items_by_code(self, db: Session, code: str):
15 | dict_id = select(DataDict.id).where(DataDict.code == code).scalar_subquery()
16 | sql = select(self.model).where(self.model.dict_id == dict_id).where(self.model.enable == 1).order_by(
17 | self.model.sort)
18 | return db.exec(sql).all()
19 |
20 | def delete_by_dict_id(self, session: Session, dict_id: int):
21 | """
22 | 通过dict_id删除对应关联的item
23 | """
24 | sql = delete(self.model).where(self.model.dict_id == dict_id)
25 | session.exec(sql)
26 |
27 | def search(self, session: Session, search: Pagination[DictItemSearch], filter_type: Optional[Dict[str, str]] = None,
28 | columns: Optional[List] = None, order_col: Optional[str] = 'id'):
29 | """
30 | 重写search函数,数据字典通过sort进行排序
31 | """
32 | sql = select(self.model).where(self.model.dict_id == search.search.dict_id)
33 | sql = self._make_search(sql, search.search, filter_type)
34 | if search.model == 'desc':
35 | sql = sql.order_by(desc(self.model.sort))
36 | else:
37 | sql = sql.order_by(self.model.sort)
38 | sql = sql.limit(search.page_size).offset((search.page - 1) * search.page_size)
39 | logger.debug(sql)
40 | return session.exec(sql).all()
41 |
42 |
43 | data_dict = CRUDDict(DataDict)
44 | dict_item = CRUDItem(DictItem)
45 |
--------------------------------------------------------------------------------
/server/crud/internal/host.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional, Dict, Union, Type
2 |
3 | import sqlalchemy.orm.exc
4 | from loguru import logger
5 | from pydantic import BaseModel
6 | from sqlmodel import select, Session, func
7 |
8 | from ...models.internal import Pagination
9 | from ...models.internal.host import Host, Group, HostGroup
10 | from ..base import CRUDBase
11 |
12 |
13 | class CRUDHost(CRUDBase[Host]):
14 | def _host_search(self, sql, q: BaseModel):
15 | """
16 | 构建主机查询语句,返回sql
17 | """
18 | logger.debug(q)
19 | sql = sql.join(HostGroup, self.model.id == HostGroup.host_id)
20 | if q.name is not None:
21 | sql = sql.where(self.model.name.like('%' + q.name + '%'))
22 | if q.ansible_host is not None:
23 | sql = sql.where(self.model.ansible_host.like('%' + q.ansible_host + '%'))
24 | if q.group_id is not None:
25 | sub_query1 = select(Group.id).where(Group.id == q.group_id)
26 | if q.ancestors is None:
27 | sub_query2 = select(Group.id).where(Group.ancestors.like(q.ancestors + ',' + str(q.group_id) + ',%'))
28 | else:
29 | sub_query2 = select(Group.id).where(Group.ancestors.like(str(q.group_id)))
30 | sql = sql.where(HostGroup.group_id.in_(sub_query1.union_all(sub_query2)))
31 |
32 | sql = sql.group_by(self.model.id).order_by(self.model.id)
33 | return sql
34 |
35 | def search_total(self, session: Session, q: BaseModel, filter_type: Optional[Dict[str, str]] = None):
36 | sql = select(self.model)
37 | sql = self._host_search(sql, q)
38 | sql = sql.subquery()
39 | count_sql = select(func.count(sql.c.id)).select_from(sql)
40 | logger.debug(count_sql)
41 | try:
42 | result = session.exec(count_sql).one()
43 | except sqlalchemy.orm.exc.NoResultFound:
44 | result = 0
45 | logger.debug(result)
46 | return result
47 |
48 | def search(self, session: Session, search: Pagination, filter_type: Optional[Dict[str, str]] = None,
49 | columns: Optional[List] = None, order_col: Optional[str] = 'id'):
50 | """
51 | 实现主机管理界面的分页查询
52 | """
53 | sql = select(self.model)
54 | sql = self._host_search(sql, search.search)
55 | sql = sql.limit(search.page_size).offset((search.page - 1) * search.page_size)
56 | logger.debug(sql)
57 | results = session.exec(sql).all()
58 | logger.debug(results)
59 | return results
60 |
61 |
62 | class CRUDGroup(CRUDBase[Group]):
63 | def search_groups(self, session: Session) -> List[Group]:
64 | sql = select(self.model)
65 | return session.exec(sql).all()
66 |
67 |
68 | host = CRUDHost(Host)
69 | group = CRUDGroup(Group)
70 |
--------------------------------------------------------------------------------
/server/crud/internal/job.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 | from loguru import logger
3 | from sqlmodel import select, Session, delete
4 | from ...models.internal.job import JobLogs
5 | from ..base import CRUDBase
6 |
7 |
8 | class CRUDJobLogs(CRUDBase[JobLogs]):
9 | def get_by_job_id(self, db: Session, job_id: str):
10 | return db.exec(select(self.model).where(self.model.job_id == job_id)).one()
11 |
12 | def delete_by_jobid(self, db: Session, job_id: str):
13 | db.exec(delete(self.model).where(self.model.job_id == job_id))
14 | db.commit()
15 |
16 |
17 | job_logs = CRUDJobLogs(JobLogs)
18 |
--------------------------------------------------------------------------------
/server/crud/internal/menu.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List
2 | from loguru import logger
3 | from sqlmodel import Session, select
4 | from ...models.internal.menu import Menu
5 | from ..base import CRUDBase
6 |
7 |
8 | class CRUDMenu(CRUDBase[Menu]):
9 | def search_menus(self, session: Session) -> List[Menu]:
10 | sql = select(self.model)
11 | sql = sql.order_by(self.model.sort)
12 | return session.exec(sql).all()
13 |
14 | def update(self, session: Session, db_obj, obj_in: Menu):
15 | """
16 | 菜单的更新,1. 更新基础内容,2. 更新apis.
17 | roles的更新,应该在角色管理里,菜单里不涉及关联角色的更新
18 | :param session:
19 | :param db_obj:
20 | :param obj_in:
21 | :return:
22 | """
23 | logger.debug(db_obj)
24 | return super(CRUDMenu, self).update(session, db_obj, obj_in)
25 |
26 | def delete(self, session: Session, id: int):
27 | db_obj = self.get(session, id)
28 | session.delete(db_obj)
29 | session.commit()
30 |
31 | def check_has_child(self, session: Session, pid: int) -> bool:
32 | """
33 | 判断是否有子菜单
34 | :param session:
35 | :param pid:
36 | :return:
37 | """
38 | sql = select(self.model).where(self.model.parent_id == pid)
39 | return session.exec(sql).first() is not None
40 |
41 |
42 | menu = CRUDMenu(Menu)
43 |
--------------------------------------------------------------------------------
/server/crud/internal/playbook.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 | from loguru import logger
3 | from sqlmodel import select, Session
4 | from ...models.internal.playbook import Playbook
5 | from ..base import CRUDBase
6 | from ...models.internal.user import UserInfo, UserLogin
7 | from .roles import role
8 |
9 |
10 | class CRUDPlaybook(CRUDBase[Playbook]):
11 |
12 | def query_playbooks(self, session: Session, query: Union[str, None] = None):
13 | """
14 | 通过名称查询playbook
15 | """
16 | sql = select(Playbook)
17 | if query:
18 | sql = sql.where(Playbook.name.like(f'%{query}%'))
19 | return session.exec(sql).all()
20 |
21 |
22 | playbook = CRUDPlaybook(Playbook)
23 |
--------------------------------------------------------------------------------
/server/crud/internal/roles.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional
2 | from loguru import logger
3 | from sqlmodel import select, Session
4 | from ...models.internal import Role, Menu, RoleMenu
5 | from ..base import CRUDBase
6 | from ...settings import casbin_enforcer
7 |
8 |
9 | class CRUDRole(CRUDBase[Role]):
10 | def get_roles_by_id(self, session: Session, roles: List[str]):
11 | return session.exec(select(self.model).where(self.model.id.in_(roles))).all()
12 |
13 | def check_exist(self, session: Session, name: str):
14 | sql = select(self.model).where(self.model.name == name)
15 | return session.exec(sql).one()
16 |
17 | def check_admin(self, session: Session, uid: int) -> bool:
18 | """
19 | 通过uid,判断此用户是否在admin组中
20 | :param session:
21 | :param uid:
22 | :return:
23 | """
24 | admin = session.exec(select(self.model).where(self.model.name == 'admin')).one()
25 | admin_users = [user.id for user in admin.users]
26 | if uid in admin_users:
27 | return True
28 | else:
29 | return False
30 |
31 | def get_all_menus(self, session: Session):
32 | return session.exec(select(Menu).where(Menu.enable == 1)).all()
33 |
34 | def get_enable_menus(self, session: Session, id: int) -> List[id]:
35 | if id is not None:
36 | sql = select(self.model).where(self.model.id == id)
37 | role: Role = session.exec(sql).one()
38 | role_menus = [menu.id for menu in role.menus]
39 | else:
40 | role_menus = []
41 | return role_menus
42 |
43 | def update_menus(self, session: Session, db_obj: Role, menus: List[int]):
44 | """
45 | 更新角色信息,还涉及到角色关联的menus
46 | :param session:
47 | :param db_obj:
48 | :param menus:
49 | :return:
50 | """
51 | logger.debug(db_obj.menus)
52 | db_menus = session.exec(select(Menu).where(Menu.id.in_(menus))).all()
53 | db_obj.menus = db_menus
54 | casbin_enforcer.delete_permissions_for_user(f'role_{db_obj.id}')
55 | logger.debug(db_menus)
56 | for menu in db_menus:
57 | if (menu.auth is not None) and menu.auth:
58 | model, act = menu.auth.split(':')
59 | logger.debug(f'增加权限:role_{db_obj.id},{model},{act}')
60 | casbin_enforcer.add_permission_for_user(f'role_{db_obj.id}', model, act)
61 | session.add(db_obj)
62 | session.commit()
63 | session.refresh(db_obj)
64 |
65 |
66 | role = CRUDRole(Role)
67 |
--------------------------------------------------------------------------------
/server/crud/internal/user.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 | from loguru import logger
3 | from sqlmodel import select, Session
4 | from ...models.internal.user import User
5 | from ..base import CRUDBase
6 | from ...models.internal.user import UserInfo, UserLogin
7 | from .roles import role
8 |
9 |
10 | class CRUDUser(CRUDBase[User]):
11 | def login(self, session: Session, login_form: UserLogin) -> User:
12 | sql = select(self.model).where(self.model.name == login_form.username,
13 | self.model.password == login_form.password,
14 | self.model.enable == 1)
15 | return session.exec(sql).one()
16 |
17 | def check_name(self, session: Session, name: str):
18 | sql = select(self.model).where(self.model.name == name)
19 | return session.exec(sql).one()
20 |
21 | def insert(self, session: Session, user_info: UserInfo) -> User:
22 | updated_user = User(**user_info.user.model_dump())
23 | user_roles = role.get_roles_by_id(session, user_info.roles)
24 | updated_user.roles = user_roles
25 | return super(CRUDUser, self).insert(session, updated_user)
26 |
27 | def update(self, session: Session, uid: int, user_info: UserInfo):
28 | db_obj = self.get(session, uid)
29 | updated_user = user_info.user
30 | db_obj = super(CRUDUser, self).update(session, db_obj, updated_user)
31 | user_roles = role.get_roles_by_id(session, user_info.roles)
32 | db_obj.roles = user_roles
33 | logger.debug('update:')
34 | logger.debug(db_obj)
35 | session.add(db_obj)
36 | session.commit()
37 | session.refresh(db_obj)
38 | return db_obj
39 |
40 | def update_passwd(self, session: Session, uid: int, passwd: str):
41 | db_obj = self.get(session, uid)
42 | db_obj.password = passwd
43 | session.add(db_obj)
44 | session.commit()
45 |
46 |
47 | user = CRUDUser(User)
48 |
--------------------------------------------------------------------------------
/server/data.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import List, Optional, Any
3 |
4 |
5 | class Common(BaseModel):
6 | code: int
7 | message: str
8 | data: Any
9 |
10 |
11 | class UserName(BaseModel):
12 | username: str
13 | disabled: bool
14 |
15 |
16 | class UserRoles(UserName):
17 | roles: List[str]
18 |
19 |
20 | class UserInfo(UserRoles):
21 | email: Optional[str]
22 | avater: Optional[str]
23 |
--------------------------------------------------------------------------------
/server/db_init.py:
--------------------------------------------------------------------------------
1 | from sqlmodel import Session
2 | from .db import init_db, engine
3 | from .models.role import Role
4 | from .models.user import User
5 |
6 |
7 | def main():
8 | init_db()
9 | with Session(engine) as session:
10 | admin_role = Role(name='admin', description='Admin管理员组', enable=1)
11 | admin = User(name='admin', enable=1, password='1111111')
12 | admin.roles.append(admin_role)
13 | session.add(admin)
14 | session.commit()
15 |
16 |
17 | if __name__ == '__main__':
18 | main()
19 |
--------------------------------------------------------------------------------
/server/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI, Depends
2 | from loguru import logger
3 | from .common.log import init_logging
4 | from .routers.internal import login, user, menu, roles, dictonary, job, host, playbook
5 | from .common.security import auth_check
6 |
7 | init_logging()
8 |
9 | app = FastAPI(dependencies=[Depends(auth_check)])
10 |
11 | app.include_router(login.router, tags=['用户登录'])
12 | app.include_router(user.router, tags=['用户管理'])
13 | app.include_router(menu.router, tags=['菜单管理'])
14 | app.include_router(roles.router, tags=['角色管理'])
15 | app.include_router(dictonary.router, tags=['数据字典'])
16 | app.include_router(job.router, tags=['任务管理'])
17 | app.include_router(host.router, tags=['主机管理'])
18 | app.include_router(playbook.router, tags=['playbook管理'])
19 |
20 |
21 | @app.on_event("startup")
22 | async def startup():
23 | """
24 | 在此时添加openapi数据的获取,导入api表,判断有没有新接口信息需要添加进去
25 | :return:
26 | """
27 | logger.debug('服务启动后执行服务')
28 |
29 |
30 | @app.on_event("shutdown")
31 | def shutdown():
32 | logger.debug('关闭服务')
33 |
--------------------------------------------------------------------------------
/server/model.conf:
--------------------------------------------------------------------------------
1 | [request_definition]
2 | r = sub, obj, act
3 |
4 | [policy_definition]
5 | p = sub, obj, act
6 |
7 | [role_definition]
8 | g = _, _
9 |
10 | [policy_effect]
11 | e = some(where (p.eft == allow))
12 |
13 | [matchers]
14 | # 指定admin组为1,则直接可以访问全部
15 | m = g(r.sub, p.sub) && keyMatch3(r.obj,p.obj) && regexMatch(r.act, p.act)
--------------------------------------------------------------------------------
/server/models/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/server/models/internal/__init__.py:
--------------------------------------------------------------------------------
1 | from .user import User
2 | from .menu import Menu
3 | from .role import Role, RoleMenu
4 | from .dictonary import DataDict, DictItem
5 | from .host import Host, Group
6 | from .job import JobLogs
7 | from .playbook import Playbook
8 | from pydantic import BaseModel
9 | from typing import TypeVar, Generic, Optional, Union
10 |
11 | T = TypeVar('T')
12 |
13 |
14 | class Pagination(BaseModel, Generic[T]):
15 | search: Union[T, None]
16 | page: Optional[int] = 1
17 | page_size: Optional[int] = 10
18 | model: Optional[str] = 'asc'
19 |
--------------------------------------------------------------------------------
/server/models/internal/dictonary.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List, Union
2 | from sqlmodel import SQLModel, Field, Relationship, Column, Boolean, Integer, String
3 |
4 |
5 | class DataDictBase(SQLModel):
6 | name: str = Field(sa_column=Column(String(50), nullable=False, comment='字典名称'))
7 | code: str = Field(sa_column=Column(String(100), nullable=False, comment='字典编号'))
8 | desc: Optional[str] = Field(sa_column=Column(String(100), default=None, comment='描述'))
9 |
10 |
11 | class DataDict(DataDictBase, table=True):
12 | __tablename__ = 'data_dict'
13 | id: Optional[int] = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True))
14 | dict_items: List["DictItem"] = Relationship(back_populates="dict")
15 |
16 |
17 | class DataDictSearch(SQLModel):
18 | name: Optional[str]
19 | code: Optional[str]
20 |
21 |
22 | class DictBase(SQLModel):
23 | label: str = Field(sa_column=Column(String(50), nullable=False, comment='名称'))
24 | value: str = Field(sa_column=Column(String(100), nullable=False, comment='数据值'))
25 | desc: Optional[str] = Field(sa_column=Column(String(100), default=None, comment='描述'))
26 | sort: Optional[int] = Field(sa_column=Column(Integer, default=1, comment='排序值,越小越靠前'))
27 | enable: bool = Field(sa_column=Column(Boolean, default=True, comment='是否启用'))
28 | dict_id: Optional[int] = Field(foreign_key="data_dict.id")
29 |
30 |
31 | class DictItem(DictBase, table=True):
32 | __tablename__ = 'dict_item'
33 |
34 | id: Optional[int] = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True))
35 | dict: Optional[DataDict] = Relationship(back_populates='dict_items')
36 |
37 |
38 | class DictRead(DictBase):
39 | id: int
40 |
41 |
42 | class DictUpdate(DictBase):
43 | id: Optional[int]
44 |
45 |
46 | class DictItemSearch(SQLModel):
47 | dict_id: int
48 | name: Union[str, None] = None
49 | data: Union[str, None] = None
50 | enable: Union[bool, None] = None
51 |
52 |
53 | class DictItemSearchFilter(SQLModel):
54 | dict_id: str
55 | label: str
56 | value: str
57 | enable: str
58 |
--------------------------------------------------------------------------------
/server/models/internal/host.py:
--------------------------------------------------------------------------------
1 | from typing import List, Union
2 | from sqlmodel import SQLModel, Field, Column, Text, Integer, String, Relationship
3 |
4 |
5 | class HostGroup(SQLModel, table=True):
6 | """
7 | 通过中间表实现:主机-组的对应关系
8 | """
9 | __tablename__ = 'host_group'
10 | host_id: int = Field(foreign_key="host.id", primary_key=True, nullable=False)
11 | group_id: int = Field(foreign_key="group.id", primary_key=True, nullable=False)
12 |
13 |
14 | class Host(SQLModel, table=True):
15 | __tablename__ = 'host'
16 | id: int = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True))
17 | name: str = Field(sa_column=Column(String(50), unique=True, nullable=False, comment='主机名'))
18 | ansible_host: str = Field(sa_column=Column(String(50), nullable=False, comment='主机地址'))
19 | ansible_port: int = Field(sa_column=Column(Integer, default=22, nullable=False, comment='ssh端口'))
20 | ansible_user: str = Field(sa_column=Column(String(50), nullable=True, default=None, comment='ssh用户名'))
21 | ansible_password: str = Field(sa_column=Column(String(50), default=None, comment='ssh密码'))
22 | ansible_ssh_private_key: str = Field(
23 | sa_column=Column('ansible_ssh_private_key', Text, default=None, nullable=True, comment='私钥'))
24 | desc: str = Field(sa_column=Column(String(100), default=None, nullable=True, comment='描述'))
25 | groups: List['Group'] = Relationship(back_populates='hosts', link_model=HostGroup)
26 |
27 |
28 | class Group(SQLModel, table=True):
29 | __tablename__ = 'group'
30 | id: int = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True))
31 | name: str = Field(sa_column=Column(String(50), nullable=False, comment='组名'))
32 | parent_id: int = Field(sa_column=Column(Integer, default=None, nullable=True, comment='父ID'))
33 | ancestors: Union[str, None] = Field(
34 | sa_column=Column(String(100), default=None, nullable=True, comment='祖先ID列表'))
35 | hosts: List['Host'] = Relationship(back_populates='groups', link_model=HostGroup)
36 |
37 |
38 | class GroupWithChild(SQLModel):
39 | id: int
40 | name: str
41 | parent_id: Union[int, None]
42 | ancestors: Union[str, None]
43 | children: List['GroupWithChild'] = []
44 |
45 |
46 | class CreateHost(SQLModel):
47 | id: Union[int, None] = None
48 | name: str
49 | groups: List[int]
50 | ansible_host: str
51 | ansible_port: int
52 | ansible_user: str
53 | ansible_password: Union[str, None] = None
54 | ansible_ssh_private_key: Union[str, None]
55 | desc: Union[str, None]
56 |
57 |
58 | class HostWithIp(SQLModel):
59 | name: Union[str, None] = None
60 | ansible_host: Union[str, None] = None
61 | group_id: Union[int, None] = None
62 | ancestors: Union[str, None]
63 |
--------------------------------------------------------------------------------
/server/models/internal/job.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from typing import Union, List, Tuple, Dict, Any, Optional
3 | from sqlmodel import SQLModel, Field, Column, Relationship, Integer, String, LargeBinary, JSON, Unicode
4 | from sqlalchemy.dialects import mysql
5 | from datetime import datetime
6 | from pydantic import BaseModel
7 |
8 |
9 | class CronJobArgs(BaseModel):
10 | cron: str
11 | start_date: Optional[str]
12 | end_date: Optional[str]
13 |
14 |
15 | class TriggerEnum(str, Enum):
16 | date = 'date'
17 | cron = 'cron'
18 |
19 |
20 | class TriggerArgs(BaseModel):
21 | run_date: Optional[str] = Field(default=None, description="date类型触发器设定执行时间,None为立即执行")
22 | cron: Optional[str] = Field(default=None, description="cron类型触发器参数")
23 | start_date: Optional[str] = Field(default=None, description="cron类型触发器开始时间")
24 | end_date: Optional[str] = Field(default=None, description="cron类型触发器结束时间")
25 |
26 |
27 | class AnsibleArgs(BaseModel):
28 | module: Union[str, None] = Field(default=None, description="ansible模块")
29 | module_args: Union[str, None] = Field(default=None, description="ansible模块参数")
30 | playbook: Union[int, None] = Field(default=None, description="ansible playbook")
31 |
32 |
33 | class JobAdd(BaseModel):
34 | id: Optional[str] = Field(default=None, description="任务ID")
35 | name: Union[str, None] = Field(description="任务名称")
36 | trigger: TriggerEnum = Field(description="触发器类型")
37 | trigger_args: TriggerArgs = Field(description="触发器")
38 | targets: List[int] = Field(description="执行任务的主机")
39 | ansible_args: AnsibleArgs = Field(description="ansible任务参数")
40 |
41 |
42 | # class Job(SQLModel, table=True):
43 | # """
44 | # 此表同步apschedule中的建表语句,如果没有,则apscheduler会自动创建对应表
45 | # """
46 | # __tablename__ = 'jobs'
47 | # id: str = Field(sa_column=Column('id', autoincrement=True, primary_key=True))
48 | # name: str = Field(sa_column=Column('name', String(50), nullable=False, unique=True))
49 | # create_time: float = Field(sa_column=Column('create_time', mysql.DOUBLE, index=True))
50 | # update_time: float = Field(sa_column=Column('update_time', mysql.DOUBLE, index=True))
51 | # job_id: str = Field(sa_column=Column('job_id', Unicode(255)))
52 | # job_logs: List["JobLog"] = Relationship(back_populates="job")
53 | #
54 | #
55 | class JobLogs(SQLModel, table=True):
56 | """
57 | 任务执行日志相关表
58 | """
59 | __tablename__ = 'job_logs'
60 | id: Optional[int] = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True))
61 | job_id: Optional[str] = Field(sa_column=Column('job_id', String(191), index=True))
62 | start_time: datetime = Field(default_factory=datetime.now, sa_column_kwargs={'comment': '任务开始时间'})
63 | end_time: datetime = Field(default=datetime.now, sa_column_kwargs={'comment': '任务结束时间'})
64 | log: str = Field(sa_column=Column(mysql.TEXT, comment='执行日志'))
65 | stats: str = Field(sa_column=Column(mysql.TEXT, comment='任务返回状态'))
66 | type: int = Field(sa_column=Column('type', mysql.TINYINT, comment='任务类型,0:cron,1:date'))
67 |
68 |
69 | class JobSearch(SQLModel):
70 | job_name: Optional[str] = None
71 | job_trigger: Optional[str] = None
72 |
73 |
74 | class JobLogSearch(BaseModel):
75 | job_id: Union[str, None] = None
76 | type: Union[int, None] = None
77 |
--------------------------------------------------------------------------------
/server/models/internal/menu.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List
2 | from sqlmodel import SQLModel, Field, Relationship, Column, Boolean, Integer, String, Float
3 | from .relationships import RoleMenu
4 |
5 |
6 | class MenuBase(SQLModel):
7 | id: Optional[int] = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True))
8 | name: str = Field(sa_column=Column(String(20), nullable=False, comment='菜单名'))
9 | icon: Optional[str] = Field(default=None, sa_column=Column(String(50), default=None, comment='Icon图标'))
10 | path: Optional[str] = Field(sa_column=Column(String(100), default=None, comment='路径'))
11 | component: Optional[str] = Field(sa_column=Column(String(50), default=None, comment='组件'))
12 | auth: Optional[str] = Field(sa_column=Column(String(50), default=None, comment='授权标识'))
13 | type: str = Field(sa_column=Column(String(10), nullable=False, comment='类型'))
14 | parent_id: Optional[int] = Field(sa_column=Column(Integer, default=None, comment='父级ID'))
15 | sort: Optional[float] = Field(default=None, sa_column=Column(Float, default=None, comment='菜单排序'))
16 | enable: bool = Field(sa_column=Column(Boolean, default=True, comment='启用'))
17 |
18 |
19 | class Menu(MenuBase, table=True):
20 | roles: List["Role"] = Relationship(back_populates="menus", link_model=RoleMenu)
21 | # apis: List['Api'] = Relationship(back_populates="menus", link_model=MenuApi)
22 |
23 |
24 | class MenusWithChild(MenuBase):
25 | children: List['MenusWithChild'] = []
26 |
27 |
28 | # 底部导入,且延迟注释
29 | from .role import Role
30 |
31 | MenusWithChild.model_rebuild()
32 |
--------------------------------------------------------------------------------
/server/models/internal/playbook.py:
--------------------------------------------------------------------------------
1 | from typing import List, Union
2 | from sqlmodel import SQLModel, Field, Column, Integer, String, TEXT
3 |
4 |
5 | class Playbook(SQLModel, table=True):
6 | id: Union[int, None] = Field(default=None, sa_column=Column('id', Integer, primary_key=True, autoincrement=True))
7 | name: str = Field(sa_column=Column(String(50), nullable=False, comment='playbook名称'))
8 | playbook: str = Field(sa_column=Column(TEXT, nullable=False, comment='playbook文件'))
9 | desc: Union[str, None] = Field(default=None,
10 | sa_column=Column(String(255), default=None, nullable=True, comment='描述'))
11 |
12 |
13 | class PlaybookSearch(SQLModel):
14 | name: Union[str, None] = None
15 |
--------------------------------------------------------------------------------
/server/models/internal/relationships.py:
--------------------------------------------------------------------------------
1 | from sqlmodel import SQLModel, Field, Unicode
2 |
3 |
4 | # 这些是权限验证的基础表,单独放置
5 | class RoleMenu(SQLModel, table=True):
6 | __tablename__ = "role_menu"
7 | role_id: int = Field(foreign_key="roles.id", primary_key=True)
8 | menu_id: int = Field(foreign_key="menu.id", primary_key=True)
9 |
10 |
11 | class UserRole(SQLModel, table=True):
12 | __tablename__ = 'user_roles'
13 |
14 | user_id: int = Field(foreign_key="user.id", primary_key=True)
15 | role_id: int = Field(foreign_key="roles.id", primary_key=True)
16 |
17 |
18 | # class UserJob(SQLModel, table=True):
19 | # """
20 | # 通过中间表实现:用户-任务的对应关系
21 | # """
22 | # __tablename__ = 'user_job'
23 | # user_id: int = Field(foreign_key="user.id", primary_key=True, nullable=False)
24 | # job_id: str = Field(Unicode(191), foreign_key="apscheduler_jobs.id", primary_key=True, nullable=False)
25 |
--------------------------------------------------------------------------------
/server/models/internal/role.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List, TYPE_CHECKING, Union
2 | from sqlmodel import SQLModel, Field, Relationship, Column, Integer, Boolean, String
3 | from .relationships import RoleMenu, UserRole
4 |
5 | if TYPE_CHECKING:
6 | from .user import User
7 | from .menu import Menu
8 |
9 |
10 | class Role(SQLModel, table=True):
11 | __tablename__ = "roles"
12 | id: Optional[int] = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True))
13 | name: Union[str, None] = Field(sa_column=Column(String(20), nullable=False, unique=True, comment='角色名'))
14 | description: Union[str, None] = Field(sa_column=Column(String(100), default=None, comment='描述'))
15 | enable: Union[bool, None] = Field(sa_column=Column(Boolean, default=True, comment='启用'))
16 | menus: List["Menu"] = Relationship(back_populates="roles", link_model=RoleMenu)
17 | users: List["User"] = Relationship(back_populates="roles", link_model=UserRole)
18 |
19 |
20 | class RoleBase(SQLModel):
21 | name: Union[str, None] = Field(max_length=20, nullable=False)
22 | description: Union[str, None] = Field(max_length=100, default=None)
23 | enable: Union[bool, None] = Field(default=True)
24 |
25 |
26 | class RoleInsert(RoleBase):
27 | menus: List[int]
28 |
29 |
30 | class RoleUpdate(RoleInsert):
31 | id: int
32 |
33 |
34 | class RoleWithMenus(RoleBase):
35 | id: int
36 | menus: List['Menu'] = []
37 |
38 |
39 | from .menu import Menu
40 |
41 | RoleWithMenus.model_rebuild()
42 |
--------------------------------------------------------------------------------
/server/models/internal/user.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List, Union, Literal, TYPE_CHECKING
2 | from pydantic import BaseModel
3 | from sqlmodel import SQLModel, Field, Relationship, Column, Integer, Boolean, String
4 | from .relationships import UserRole
5 | from .role import Role
6 |
7 | if TYPE_CHECKING:
8 | from .role import Role
9 | from .job import Job
10 |
11 |
12 | class User(SQLModel, table=True):
13 | __tablename__ = 'user'
14 | id: Optional[int] = Field(sa_column=Column('id', Integer, primary_key=True, autoincrement=True))
15 | name: Union[str, None] = Field(sa_column=Column(String(20), nullable=False, unique=True, comment='用户名'))
16 | enable: Union[bool, None] = Field(sa_column=Column(Boolean, default=True, comment='可用'))
17 | avatar: Union[str, None] = Field(sa_column=Column(String(100), default=None, comment='头像'))
18 | email: Union[str, None] = Field(sa_column=Column(String(20), default=None, comment='邮箱'))
19 | password: Optional[str] = Field(sa_column=Column(String(50), comment='密码'))
20 | roles: List['Role'] = Relationship(back_populates="users", link_model=UserRole)
21 |
22 |
23 | class UserWithOutPasswd(SQLModel):
24 | name: Union[str, None] = Field(max_length=20, nullable=False)
25 | enable: Union[bool, None] = Field(default=True)
26 | avatar: Union[str, None] = Field(max_length=100, default=None)
27 | email: Union[str, None] = Field(max_length=20, default=None)
28 |
29 |
30 | class UserBase(UserWithOutPasswd):
31 | password: Optional[str] = Field(sa_column=Column(String(50), comment='密码'))
32 | # age: Optional[int] = Field(..., title='年龄', lt=120)
33 |
34 |
35 | class UserRead(UserWithOutPasswd):
36 | # get请求时返回的数据模型,response_model使用模型
37 | id: int
38 |
39 |
40 | class UserReadWithRoles(UserRead):
41 | # 包含relationship的数据模型
42 | roles: List['Role'] = []
43 |
44 |
45 | class UserRoles(SQLModel):
46 | roles: List['Role'] = []
47 | enable: List[str] = []
48 |
49 |
50 | class UserCreateWithRoles(SQLModel):
51 | # POST请求时,传递过来的模型
52 | user: UserBase
53 | roles: List[int]
54 |
55 | class Config:
56 | title = '新建用户'
57 |
58 |
59 | class UserUpdatePassword(SQLModel):
60 | id: int
61 | password: str
62 |
63 |
64 | class UserUpdateWithRoles(SQLModel):
65 | # PUT请求时,传递过来的数据模型
66 | user: UserWithOutPasswd
67 | roles: List[int]
68 |
69 |
70 | class UserLogin(SQLModel):
71 | username: str
72 | password: str
73 |
74 |
75 | class LoginResponse(SQLModel):
76 | uid: int
77 | token: str
78 |
79 |
80 | class UserInfo(BaseModel):
81 | user: User
82 | roles: List[str]
83 |
--------------------------------------------------------------------------------
/server/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/requirements.txt
--------------------------------------------------------------------------------
/server/routers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/routers/__init__.py
--------------------------------------------------------------------------------
/server/routers/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/routers/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/server/routers/__pycache__/menu.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/routers/__pycache__/menu.cpython-39.pyc
--------------------------------------------------------------------------------
/server/routers/__pycache__/roles.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/routers/__pycache__/roles.cpython-39.pyc
--------------------------------------------------------------------------------
/server/routers/__pycache__/user.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/routers/__pycache__/user.cpython-39.pyc
--------------------------------------------------------------------------------
/server/routers/internal/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/routers/internal/__init__.py
--------------------------------------------------------------------------------
/server/routers/internal/dictonary.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List
2 | from fastapi import APIRouter, Depends, status, HTTPException
3 | from sqlmodel import Session
4 | from ...models.internal import Pagination
5 | from ...models.internal.dictonary import DataDict, DictRead, DictUpdate, DictItem, DataDictSearch, \
6 | DictItemSearch, DictItemSearchFilter
7 | from ...common.response_code import ApiResponse, SearchResponse
8 | from ...common.database import get_session
9 | from ... import crud
10 |
11 | router = APIRouter(prefix='/api')
12 |
13 |
14 | @router.post('/dict/item/search', summary="字典列表查询", response_model=ApiResponse[SearchResponse[DictRead]])
15 | async def search_items(search: Pagination[DictItemSearch], session: Session = Depends(get_session)):
16 | filter_type = DictItemSearchFilter(dict_id='eq', label='like', enable='eq', value='like')
17 | total = crud.internal.dict_item.search_total(session, search.search, filter_type.model_dump())
18 | items: List[DictRead] = crud.internal.dict_item.search(session, search, filter_type.model_dump())
19 | item_list = [DictRead.from_orm(item) for item in items]
20 | return ApiResponse(
21 | data={
22 | 'total': total,
23 | 'data': item_list
24 | }
25 |
26 | )
27 |
28 |
29 | @router.post('/dict/item', summary="添加字典字段", response_model=ApiResponse[DictRead])
30 | async def add_dict_item(dict_item: DictUpdate, session: Session = Depends(get_session)):
31 | new_item = crud.internal.dict_item.insert(session, DictItem(**dict_item.model_dump()))
32 | return ApiResponse(
33 | data=DictRead.from_orm(new_item)
34 | )
35 |
36 |
37 | @router.put('/dict/item', summary="更新字典元素", response_model=ApiResponse)
38 | async def update_dict_item(dict_item: DictUpdate, session: Session = Depends(get_session)):
39 | db_obj = crud.internal.dict_item.get(session, dict_item.id)
40 | crud.internal.dict_item.update(session, db_obj, dict_item)
41 | return ApiResponse()
42 |
43 |
44 | @router.delete('/dict/item/{item_id}', summary="删除字典元素", )
45 | async def del_dict_item(item_id: int, session: Session = Depends(get_session)):
46 | crud.internal.dict_item.delete(session, item_id)
47 | return ApiResponse()
48 |
49 |
50 | @router.get("/dict/{dict_code}", summary="获取数据字典", response_model=ApiResponse[List[DictRead]],
51 | response_model_exclude={'data': {'__all__': {'desc', 'sort', 'enable'}}})
52 | async def get_dict(dict_code: str, session: Session = Depends(get_session)):
53 | dict_items: List[DictItem] = crud.internal.dict_item.get_items_by_code(session, dict_code)
54 | if dict_items:
55 | return ApiResponse(
56 | data=[DictRead.from_orm(item) for item in dict_items]
57 | )
58 | else:
59 | return ApiResponse(
60 | code=404,
61 | message=f"无效的数据字典:{dict_code}"
62 | )
63 |
64 |
65 | @router.delete("/dict/{dict_id}", summary="删除数据字典")
66 | async def del_dict(dict_id: int, session: Session = Depends(get_session)):
67 | try:
68 | crud.internal.dict_item.delete_by_dict_id(session, dict_id)
69 | crud.internal.data_dict.delete(session, dict_id)
70 | return ApiResponse()
71 | except Exception as e:
72 | return ApiResponse(
73 | code=500,
74 | message=f"删除数据字典失败:{e}"
75 | )
76 |
77 |
78 | @router.post("/dict", summary="新建数据字典", response_model=ApiResponse[DataDict])
79 | async def add_dict(data_dict: DataDict, session: Session = Depends(get_session)):
80 | obj = crud.internal.data_dict.insert(session, data_dict)
81 | return ApiResponse(
82 | data=obj
83 | )
84 |
85 |
86 | @router.put("/dict", summary="更新数据字典", response_model=ApiResponse[DataDict])
87 | async def add_dict(data_dict: DataDict, session: Session = Depends(get_session)):
88 | db_obj = crud.internal.data_dict.get(session, data_dict.id)
89 | obj = crud.internal.data_dict.update(session, db_obj, data_dict)
90 | return ApiResponse(
91 | data=obj
92 | )
93 |
94 |
95 | @router.post('/dict/search',
96 | summary="查询数据字典")
97 | async def get_dicts(search: Pagination[DataDictSearch], session: Session = Depends(get_session)):
98 | filter_type = DataDictSearch(name='like', code='like')
99 | total = crud.internal.data_dict.search_total(session, search.search, filter_type.model_dump())
100 | dicts: List[DataDict] = crud.internal.data_dict.search(session, search, filter_type.model_dump())
101 | return ApiResponse(
102 | data={
103 | 'total': total,
104 | 'data': dicts
105 | }
106 | )
107 |
--------------------------------------------------------------------------------
/server/routers/internal/login.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from loguru import logger
3 | from fastapi import APIRouter, Depends, Request, status
4 | from ...common.database import get_session
5 | from sqlmodel import Session, select
6 | from sqlalchemy.exc import NoResultFound
7 | from ...common.response_code import ApiResponse
8 | from ...common.security import create_access_token
9 | from ...models.internal import User, Menu
10 | from ...common.dep import get_uid
11 | from ...models.internal.user import UserLogin, LoginResponse
12 | from ... import crud
13 | from ...common.utils import menu_convert
14 |
15 | router = APIRouter(prefix='/api')
16 |
17 |
18 | @router.post('/login', summary="登录验证", response_model=ApiResponse[LoginResponse])
19 | async def login(login_form: UserLogin, session: Session = Depends(get_session)):
20 | """
21 | 处理登录请求,返回{token:xxxxx},判断用户密码是否正确
22 | :param login_form:
23 | :param session
24 | :return:
25 | """
26 | try:
27 | user = crud.internal.user.login(session, login_form)
28 | except NoResultFound:
29 | return ApiResponse(
30 | code=status.HTTP_400_BAD_REQUEST,
31 | message='用户名或密码错误',
32 | )
33 | user_roles = []
34 | for role in user.roles:
35 | if role.enable == 1:
36 | user_roles.append(role.id)
37 | # 把roles封装再token里,每次只需要depends检查对应的roles是否有权限即可
38 | access_token = create_access_token(
39 | data={"uid": user.id}
40 | )
41 | return ApiResponse(
42 | data={"uid": user.id,
43 | "token": access_token}
44 | )
45 |
46 |
47 | @router.get('/permission', summary='获取权限')
48 | async def get_permission(uid: int = Depends(get_uid), session: Session = Depends(get_session)):
49 | """
50 | 用户权限请求,返回拥有权限的菜单列表,前端根据返回的菜单列表信息,合成菜单项
51 | :param request:
52 | :param session:
53 | :param token:
54 | :return:
55 | """
56 | logger.debug(f"uid is:{uid}")
57 | user: User = crud.internal.user.get(session, uid)
58 | logger.debug(user.roles)
59 | user_menus = []
60 | # admin组用户获取所有菜单列表
61 | if uid == 1 or crud.internal.role.check_admin(session, uid):
62 | menu_list = session.exec(select(Menu).where(Menu.type != 'btn', Menu.enable == 1).order_by(Menu.sort)).all()
63 | btn_list = session.exec(
64 | select(Menu.auth).where(Menu.type == 'btn', Menu.enable == 1).where(Menu.auth.is_not(None))).all()
65 | else:
66 | for role in user.roles:
67 | user_menus.extend([menu.id for menu in role.menus])
68 | menu_list = session.exec(
69 | select(Menu).where(Menu.id.in_(set(user_menus))).where(Menu.type != 'btn').where(Menu.enable == 1).order_by(
70 | Menu.sort)).all()
71 | btn_list = session.exec(select(Menu.auth).where(Menu.id.in_(set(user_menus))).where(Menu.type == 'btn').where(
72 | Menu.enable == 1)).all()
73 | user_menus = menu_convert(menu_list)
74 |
75 | logger.debug(f"user menus:{user_menus}")
76 | return ApiResponse(
77 | data={
78 | 'menus': user_menus,
79 | 'btns': btn_list
80 | }
81 | )
82 |
--------------------------------------------------------------------------------
/server/routers/internal/menu.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List
2 | from fastapi import APIRouter, Depends, status, HTTPException
3 | from sqlmodel import Session
4 | from ...common.response_code import ApiResponse
5 | from ...common.database import get_session
6 | from ...common.auth_casbin import Authority
7 | from ...models.internal.menu import MenuBase, Menu, MenusWithChild
8 | from ...common import utils
9 | from ... import crud
10 | from ...settings import casbin_enforcer
11 |
12 | router = APIRouter(prefix='/api')
13 |
14 |
15 | @router.get('/menus', summary="列出菜单", response_model=ApiResponse[List[MenusWithChild]])
16 | async def get_all_menu(session: Session = Depends(get_session)):
17 | # 复用crud.get_menu_list,默认role为admin就是返回所有的菜单列表
18 | menu_list: List[Menu] = crud.menu.search_menus(session)
19 | user_menus = utils.menu_convert(menu_list)
20 | return ApiResponse(
21 | data=user_menus
22 | )
23 |
24 |
25 | @router.post('/menus', summary="新建菜单", response_model=ApiResponse[Menu],
26 | dependencies=[Depends(Authority("menu:add"))])
27 | async def add_menu(menu: MenuBase, session: Session = Depends(get_session)):
28 | """
29 | # 新建的菜单,还是没有授权给角色的,所以直接新增就行了
30 | :param menu:
31 | :param session:
32 | :return:
33 | """
34 | db_obj = crud.menu.insert(session, Menu(**menu.model_dump()))
35 | session.add(db_obj)
36 | session.commit()
37 | session.refresh(db_obj)
38 | return ApiResponse(
39 | data=db_obj
40 | )
41 |
42 |
43 | @router.put('/menus', summary="更新菜单", response_model=ApiResponse[Menu],
44 | dependencies=[Depends(Authority("menu:update"))])
45 | async def update_menu(menu: MenuBase, session: Session = Depends(get_session)):
46 | """
47 | 更新菜单,涉及到原菜单对应api的更新,则需要更新对应信息
48 | :param menu:
49 | :param session:
50 | :return:
51 | """
52 | db_obj: Menu = crud.menu.get(session, menu.id)
53 | new_obj: Menu = crud.menu.update(session, db_obj, menu)
54 | session.add(new_obj)
55 | session.commit()
56 | session.refresh(new_obj)
57 | return ApiResponse(
58 | data=new_obj
59 | )
60 |
61 |
62 | @router.delete('/menus/{menu_id}', summary='删除菜单',
63 | dependencies=[Depends(Authority("menu:del"))])
64 | async def del_menu(menu_id: int, session: Session = Depends(get_session)):
65 | db_obj = crud.menu.get(session, menu_id)
66 | if len(db_obj.roles) > 0:
67 | roles = [role.name for role in db_obj.roles]
68 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"{roles} 角色关联菜单,请先取消关联")
69 | if crud.menu.check_has_child(session, menu_id):
70 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"菜单下存在子菜单,请先删除子菜单")
71 | crud.menu.delete(session, menu_id)
72 | return ApiResponse()
73 |
--------------------------------------------------------------------------------
/server/routers/internal/playbook.py:
--------------------------------------------------------------------------------
1 | from typing import List, Union
2 | from loguru import logger
3 | from fastapi import APIRouter, Depends
4 | from sqlmodel import Session
5 | from server.common.response_code import ApiResponse, SearchResponse
6 | from server.common.database import get_session
7 | from server.models.internal.playbook import Playbook, PlaybookSearch
8 | from server.models.internal import Pagination
9 | from server import crud
10 |
11 | router = APIRouter(prefix='/api')
12 |
13 |
14 | @router.get('/playbook/{playbook_id}', summary='获取playbook详情', response_model=ApiResponse[Playbook])
15 | async def get_playbook_by_id(playbook_id: int, session: Session = Depends(get_session)):
16 | playbook = crud.internal.playbook.get(session, playbook_id)
17 | return ApiResponse(data=playbook.model_dump())
18 |
19 |
20 | @router.post('/playbook/search', summary="获取playbook列表", response_model=ApiResponse[SearchResponse[Playbook]])
21 | async def get_all_user(search: Pagination[PlaybookSearch],
22 | session: Session = Depends(get_session)):
23 | """
24 | :param search: Pagination实例,包含搜索的所有参数 偏移页面
25 | :param session:
26 | :return:
27 | """
28 | total = crud.internal.playbook.search_total(session, search.search, {'name': 'like'})
29 | logger.debug(total)
30 | playbooks: List[Playbook] = crud.internal.playbook.search(session, search, {'name': 'like'})
31 | playbook_list = [playbook.model_dump(exclude={'playbook'}) for playbook
32 | in playbooks]
33 | logger.debug(playbook_list)
34 | return ApiResponse(
35 | data={
36 | 'total': total,
37 | 'data': playbook_list
38 | }
39 | )
40 |
41 |
42 | @router.get('/playbook', summary='获取playbooks列表', response_model=ApiResponse[List[Playbook]])
43 | async def query_playbooks(query: Union[str, None] = None, session: Session = Depends(get_session)):
44 | playbooks: List[Playbook] = crud.internal.playbook.query_playbooks(session, query)
45 | playbook_list = [playbook.model_dump(exclude={'playbook'}) for playbook
46 | in playbooks]
47 | return ApiResponse(data=playbook_list)
48 |
49 |
50 | @router.post('/playbook', summary='创建playbook')
51 | async def create_playbook(playbook: Playbook, session: Session = Depends(get_session)):
52 | logger.debug(playbook)
53 | crud.internal.playbook.insert(session, Playbook(**playbook.model_dump(exclude_unset=True)))
54 | return ApiResponse()
55 |
56 |
57 | @router.put('/playbook', summary='更新playbook')
58 | async def update_playbook(playbook: Playbook, session: Session = Depends(get_session)):
59 | crud.internal.playbook.update(session, crud.internal.playbook.get(session, playbook.id),
60 | Playbook(**playbook.model_dump(exclude_unset=True)))
61 | return ApiResponse()
62 |
63 |
64 | @router.delete('/playbook/{book_id}', summary='删除playbook')
65 | async def delete_playbook(book_id: int, session: Session = Depends(get_session)):
66 | crud.internal.playbook.delete(session, book_id)
67 | return ApiResponse()
68 |
--------------------------------------------------------------------------------
/server/routers/internal/roles.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List
2 | from loguru import logger
3 | from fastapi import APIRouter, Depends, status
4 | from fastapi.exceptions import HTTPException
5 | from sqlmodel import Session
6 | from sqlalchemy.exc import NoResultFound
7 | from ...common.response_code import ApiResponse
8 | from ...common.auth_casbin import Authority
9 | from ...common.database import get_session
10 | from ... import crud
11 | from ...models.internal import Role, Menu
12 | from ...models.internal.role import RoleBase, RoleWithMenus, RoleInsert, RoleUpdate
13 | from server.models.internal import Pagination
14 | from ...common.utils import menu_convert
15 |
16 | router = APIRouter(prefix='/api')
17 |
18 |
19 | @router.get('/roles/enable-menus', summary='获取角色菜单')
20 | async def get_role_menus(id: Optional[int] = None, session: Session = Depends(get_session)):
21 | """
22 | 返回菜单,和已分配权限菜单
23 | :param id:
24 | :param session:
25 | :return:
26 | """
27 | # 所有角色,进行权限分配的时候,都是返回所有菜单列表,enable=True:只查询启用的菜单
28 | menu_list: List[Menu] = crud.internal.role.get_all_menus(session)
29 | user_menus = menu_convert(menu_list)
30 | role_menus = crud.internal.role.get_enable_menus(session, id)
31 | return ApiResponse(
32 | data={
33 | "menus": user_menus,
34 | "enable": role_menus
35 | }
36 | )
37 |
38 |
39 | @router.post('/roles/search',
40 | summary="查询角色")
41 | async def get_roles(search: Pagination[RoleBase], session: Session = Depends(get_session)):
42 | total = crud.internal.role.search_total(session, search.search, {'name': 'like', 'enable': 'eq'})
43 | logger.debug(total)
44 | roles: List[Role] = crud.internal.role.search(session, search, {'name': 'like', 'enable': 'eq'})
45 | role_with_menus: List[RoleWithMenus] = []
46 | for role in roles:
47 | new_role = RoleWithMenus(**role.model_dump(), menus=role.menus)
48 | role_with_menus.append(new_role)
49 | return ApiResponse(
50 | data={
51 | 'total': total,
52 | 'data': role_with_menus
53 | }
54 | )
55 |
56 |
57 | @router.get('/roles/exist', summary='角色是否存在')
58 | async def check_uname_exist(name: str, session: Session = Depends(get_session)):
59 | try:
60 | crud.internal.role.check_exist(session, name)
61 | except NoResultFound:
62 | return ApiResponse()
63 | else:
64 | return ApiResponse(
65 | message='error',
66 | data='error'
67 | )
68 |
69 |
70 | @router.post('/roles', summary="新建角色", response_model=ApiResponse[Role],
71 | dependencies=[Depends(Authority('role:add'))])
72 | async def add_roles(role_info: RoleInsert, session: Session = Depends(get_session)):
73 | logger.debug(role_info)
74 | enable_menus = role_info.menus
75 | delattr(role_info, 'menus')
76 | try:
77 | db_obj = crud.internal.role.insert(session, Role(**role_info.model_dump()))
78 | crud.internal.role.update_menus(session, db_obj, enable_menus)
79 | return ApiResponse(
80 | data=db_obj
81 | )
82 | except Exception as e:
83 | logger.error(f"add role error:{str(e)}")
84 | return ApiResponse(
85 | code=500,
86 | message=f"新建角色错误"
87 | )
88 |
89 |
90 | @router.put('/roles', summary="更新角色", response_model=ApiResponse[Role],
91 | dependencies=[Depends(Authority('role:update'))])
92 | async def update_roles(role_info: RoleUpdate, session: Session = Depends(get_session)):
93 | logger.debug(role_info)
94 | if role_info.name == 'admin':
95 | ApiResponse(code=status.HTTP_400_BAD_REQUEST, message='admin权限组无法更新信息')
96 | db_obj = crud.internal.role.get(session, role_info.id)
97 | enable_menus = role_info.menus
98 | delattr(role_info, 'menus')
99 | db_obj = crud.internal.role.update(session, db_obj, role_info)
100 | crud.internal.role.update_menus(session, db_obj, enable_menus)
101 | return ApiResponse(
102 | data=db_obj
103 | )
104 |
105 |
106 | @router.delete('/roles/{id}', summary='删除角色', dependencies=[Depends(Authority('role:del'))],
107 | status_code=status.HTTP_204_NO_CONTENT)
108 | async def del_role(id: int, session: Session = Depends(get_session)):
109 | db_obj = crud.internal.role.get(session, id)
110 | if db_obj.name == 'admin':
111 | raise HTTPException(status_code=400, detail='admin用户组无法删除')
112 | if len(db_obj.users) > 0:
113 | raise HTTPException(status_code=400, detail='有用户关联此角色')
114 | crud.internal.role.delete(session, id)
115 |
--------------------------------------------------------------------------------
/server/settings.py:
--------------------------------------------------------------------------------
1 | import yaml
2 | import os
3 | from pathlib import Path
4 | import casbin_sqlalchemy_adapter
5 | import casbin
6 | from sqlmodel import create_engine
7 |
8 | # 获取环境变量,默认为 development
9 | env = os.getenv('ENV', 'development')
10 | config_path = Path(__file__).parent.parent / 'config' / f'{env}.yaml'
11 |
12 | # 读取配置文件
13 | with open(config_path, 'r', encoding='utf-8') as f:
14 | config = yaml.safe_load(f)
15 | settings = config['server']
16 |
17 | engine = create_engine(
18 | str(settings['database_uri']),
19 | pool_size=5,
20 | max_overflow=10,
21 | pool_timeout=30,
22 | pool_pre_ping=True
23 | )
24 | adapter = casbin_sqlalchemy_adapter.Adapter(engine)
25 | casbin_enforcer = casbin.Enforcer(settings['casbin_model_path'], adapter)
26 |
--------------------------------------------------------------------------------
/server/sql/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/sql/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/server/sql/__pycache__/crud.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/sql/__pycache__/crud.cpython-39.pyc
--------------------------------------------------------------------------------
/server/sql/__pycache__/database.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/sql/__pycache__/database.cpython-39.pyc
--------------------------------------------------------------------------------
/server/sql/__pycache__/models.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/sql/__pycache__/models.cpython-39.pyc
--------------------------------------------------------------------------------
/server/sql/__pycache__/schemas.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/sql/__pycache__/schemas.cpython-39.pyc
--------------------------------------------------------------------------------
/server/static/template/system.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/server/static/template/system.xlsx
--------------------------------------------------------------------------------
/service.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # 生产、开发环境服务控制
3 | # service.sh dev|pro start|stop|restart
4 |
5 | # 获取脚本所在目录的绝对路径
6 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
7 |
8 | start_fastapi() {
9 | env=$1
10 | if pgrep -f "gunicorn server.main:app" > /dev/null || pgrep -f "uvicorn server.main:app" > /dev/null; then
11 | echo "FastAPI service is already running."
12 | return 1
13 | fi
14 |
15 | echo "Starting FastAPI service..."
16 | cd "$SCRIPT_DIR"
17 |
18 | if [ "$env" = "pro" ]; then
19 | echo "Running in production mode..."
20 | export ENV=production
21 | nohup gunicorn server.main:app \
22 | -b 127.0.0.1:8000 \
23 | -w 4 \
24 | -k uvicorn.workers.UvicornWorker \
25 | > gunicorn.log 2>&1 &
26 | else
27 | echo "Running in development mode..."
28 | export ENV=development
29 | nohup python -m uvicorn server.main:app \
30 | --host 0.0.0.0 --port 8000 \
31 | --reload \
32 | --reload-exclude www \
33 | > fastapi_dev.log 2>&1 &
34 | fi
35 | }
36 |
37 | stop_fastapi() {
38 | if ! pgrep -f "gunicorn server.main:app" > /dev/null && ! pgrep -f "uvicorn server.main:app" > /dev/null; then
39 | echo "FastAPI service is not running."
40 | return 1
41 | fi
42 | echo "Stopping FastAPI service..."
43 | pkill -f "gunicorn.*server.main:app"
44 | pkill -9 -f "python.*-m.*uvicorn.*server.main:app"
45 | # 关闭所有相关的 Python 子进程
46 | pkill -9 -f "multiprocessing-fork"
47 | # 确保端口被释放
48 | fuser -k 8000/tcp
49 | }
50 |
51 | start_scheduler() {
52 | echo "Starting RPyC scheduler..."
53 | cd "$SCRIPT_DIR"
54 | if [ "$1" = "pro" ]; then
55 | export ENV=production
56 | else
57 | export ENV=development
58 | fi
59 | export PYTHONPATH="$SCRIPT_DIR:$PYTHONPATH"
60 | nohup python rpyc_scheduler/server.py > rpyc_scheduler.log 2>&1 &
61 | }
62 |
63 | stop_scheduler() {
64 | echo "Stopping RPyC scheduler..."
65 | pkill -f "python.*server.py"
66 | }
67 |
68 | case "$2" in
69 | start)
70 | start_fastapi "$1"
71 | start_scheduler "$1"
72 | ;;
73 | stop)
74 | stop_fastapi
75 | stop_scheduler
76 | ;;
77 | restart)
78 | stop_fastapi
79 | stop_scheduler
80 | sleep 2
81 | start_fastapi "$1"
82 | start_scheduler "$1"
83 | ;;
84 | *)
85 | echo "Usage: $0 {dev|pro} {start|stop|restart}"
86 | echo "Options:"
87 | echo " dev Development environment"
88 | echo " pro Production environment"
89 | echo "Commands:"
90 | echo " start Start services"
91 | echo " stop Stop services"
92 | echo " restart Restart services"
93 | exit 1
94 | ;;
95 | esac
96 |
--------------------------------------------------------------------------------
/todo-list.md:
--------------------------------------------------------------------------------
1 | # Bug列表
2 | * 登录页面,不存在用户登录能继续执行后续的user_info请求
3 | * 用户退出没有清理asyncRouter信息,导致下个用户登录不需要请求获取列表信息---logout增加清理asyncRouter
4 | * 前端页面刷新导致vuex store信息丢失
5 |
6 | # 功能需求
7 | * 用户登录获取权限列表
--------------------------------------------------------------------------------
/www/.env.development:
--------------------------------------------------------------------------------
1 | NODE_ENV='development'
2 | VITE_APP_BASEURL='http://localhost'
3 | VITE_APP_WS='ws://localhost'
--------------------------------------------------------------------------------
/www/.env.developmentv:
--------------------------------------------------------------------------------
1 | NODE_ENV='development'
2 | VITE_APP_BASEURL='http://localhost'
--------------------------------------------------------------------------------
/www/.env.production:
--------------------------------------------------------------------------------
1 | NODE_ENV=production
2 | VITE_APP_BASEURL='http://172.16.8.201:8000'
--------------------------------------------------------------------------------
/www/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/www/README.md:
--------------------------------------------------------------------------------
1 | # 自封组件介绍
2 | ## 分页查询
3 | ```javascript
4 | // 定义查询字段,type字段定义各个查询字段的方式
5 | /*
6 | * type可用参数:
7 | * 'r_like':右like模糊查询
8 | * 'like':全模糊查询
9 | * 'l_like':左like模糊查询
10 | * 'eq':等于
11 | * 'ne':不等于
12 | * 'lt':小于
13 | * 'le':小于等于
14 | * 'gt':大于
15 | * 'ge':大于等于
16 | * */
17 | const searchForm = {
18 | tags: null,
19 | path: null,
20 | method: null,
21 | summary: null,
22 | type: {
23 | tags: 'r_like',
24 | path: 'like',
25 | method: 'eq',
26 | summary: 'like',
27 | }
28 | }
29 |
30 | /*usePagination(url,searchForm,orderModel)
31 | *
32 | * */
33 | const {
34 | search,
35 | tableData,
36 | currentPage,
37 | pageSize,
38 | orderModel,
39 | total,
40 | freshCurrentPage,
41 | handleSearch
42 | } = usePagination('/api/sysapis', searchForm,'desc')
43 | ```
44 |
45 | ## AutoFormDialog
46 | ### 调用
47 | ```
48 |
50 |
51 | ```
52 | ### 传递属性:
53 |
54 | * 动态表单组件
55 | * @description 动态表单组件
56 | * @tutorial 无
57 | * @property {Boolean} visible 是否显示formdialog
58 | * @property {Number} col = [1|2] 显示列数
59 | * @property {String} labelWidth = "100px" formItem label的宽度
60 | * @property {Array} formItemInfo 表单各字段的属性
61 | * @event {Function} update 提交事件,返回新值对象:{prop1:value,prop2:value,prop3:value}
62 | * @example
63 |
64 |
65 | ### formItemInfo信息
66 | ```
67 | const formItemInfo = [{
68 | "type": "text", #用于判断类型
69 | "prop": "name", #对应绑定form的prop属性值名称,后期返回数据也是此字段名
70 | "value": null, #传递的默认值
71 | "label": "菜单名", #formItem的label
72 | # 对输入字段设置对应的rules验证规则
73 | "rules": [{
74 | "required": true,
75 | "message": "请输入菜单名",
76 | "trigger": "blur"
77 | }],
78 | # formItem中对应表单组件的属性值,直接传递给相应组件
79 | "properties": {
80 | "placeholder": "请输入菜单名",
81 | },
82 | },
83 | {
84 | "type": "number",
85 | "prop": "age",
86 | "value": 11,
87 | "label": "年龄",
88 | "rules": [{
89 | "required": true,
90 | "message": "请输入年龄",
91 | "trigger": "blur"
92 | }],
93 | "properties": {
94 | "placeholder": "请输入年龄",
95 | },
96 | },
97 | {
98 | "type": "switch",
99 | "prop": "value",
100 | "value": true,
101 | "label": "值",
102 | "rules": [{
103 | "required": true,
104 | "message": "请输入值",
105 | "trigger": "blur"
106 | }],
107 |
108 | },
109 | {
110 | "type": "date",
111 | "prop": "date",
112 | "value": "2021-11-12",
113 | "label": "创建时间",
114 | "rules": [{
115 | "required": true,
116 | "message": "参考前端组件填写",
117 | "trigger": "blur"
118 | }],
119 | "properties": {
120 | "type": 'date',
121 | "placeholder": '请输入日期',
122 | "value-format": "YYYY-MM-DD"
123 | },
124 | },
125 | {
126 | "type": "datetime",
127 | "prop": "datetime",
128 | "value": "2021-12-12 12:12:12",
129 | "label": "申报时间",
130 | "rules": [{
131 | "required": true,
132 | "message": "参考前端组件填写",
133 | "trigger": "blur"
134 | }],
135 | "properties": {
136 | "type": 'datetime',
137 | "placeholder": '请输入时间',
138 | "value-format": "YYYY-MM-DD HH:mm:ss",
139 | "style": "width: 100%;"
140 | },
141 | }
142 | ]
143 | ```
144 |
--------------------------------------------------------------------------------
/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | fastapi-admin
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/www/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "www",
3 | "version": "0.1.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@element-plus/icons-vue": "^2.3.1",
13 | "@vueuse/core": "^10.7.0",
14 | "ace-builds": "^1.35.4",
15 | "axios": "^1.7.9",
16 | "core-js": "^3.40.0",
17 | "echarts": "^5.4.2",
18 | "element-plus": "^2.9.3",
19 | "js-base64": "^3.7.5",
20 | "js-cookie": "^3.0.5",
21 | "jsonref": "^8.0.8",
22 | "pinia": "2.3.0",
23 | "v-contextmenu": "^3.2.0",
24 | "vue": "^3.5.13",
25 | "vue-draggable-plus": "^0.3.5",
26 | "vue-echarts": "^6.6.5",
27 | "vue-router": "^4.5.0",
28 | "vue3-ace-editor": "^2.2.4",
29 | "xterm": "^5.3.0",
30 | "xterm-addon-attach": "^0.9.0",
31 | "xterm-addon-fit": "^0.8.0"
32 | },
33 | "devDependencies": {
34 | "@rollup/plugin-dynamic-import-vars": "^2.1.2",
35 | "@vitejs/plugin-vue": "^5.2.1",
36 | "js-md5": "^0.8.3",
37 | "sass": "^1.79.3",
38 | "unocss": "^65.4.0",
39 | "unplugin-auto-import": "^0.17.2",
40 | "unplugin-vue-components": "^0.26.0",
41 | "vite": "^6.0.7",
42 | "vite-plugin-dynamic-import": "^1.6.0"
43 | },
44 | "eslintConfig": {
45 | "root": true,
46 | "env": {
47 | "node": true
48 | },
49 | "extends": [
50 | "plugin:vue/vue3-essential"
51 | ],
52 | "parserOptions": {
53 | "parser": "@babel/eslint-parser"
54 | },
55 | "rules": {
56 | "vue/multi-word-component-names": 0
57 | }
58 | },
59 | "browserslist": [
60 | "> 1%",
61 | "last 2 versions",
62 | "not dead"
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/www/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/www/public/favicon.ico
--------------------------------------------------------------------------------
/www/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
23 |
--------------------------------------------------------------------------------
/www/src/api/dictonary.js:
--------------------------------------------------------------------------------
1 | import {GET, POST, PUT, DELETE} from '@/utils/request'
2 |
3 | export const PostNewDict = (dict) => POST('/api/dict', dict)
4 | export const PutDict = (dict) => PUT('/api/dict', dict)
5 | export const DelDict = (id) => DELETE('/api/dict/' + id)
6 | export const PostNewDictItem = (item) => POST('/api/dict/item', item)
7 | export const PutDictItem = (item) => PUT('/api/dict/item', item)
8 | export const DelDictItem = (id) => DELETE('/api/dict/item/' + id)
9 | export const GetDictItems = (code) => GET('/api/dict/' + code)
10 |
--------------------------------------------------------------------------------
/www/src/api/file.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function downloadFile(url,method,data=null){
4 | let config = {
5 | url:url,
6 | method:method,
7 | responseType:"blob"
8 | }
9 | if(method === "post"){
10 | config['data']= data
11 | }
12 | request(config).then((res)=>{
13 | let url = window.URL.createObjectURL(new Blob([res.data]))
14 | let a = document.createElement('a')
15 | a.style.display = 'none'
16 | a.href = url
17 | a.setAttribute('download',res.filename)
18 | document.body.appendChild(a)
19 | a.click()
20 | document.body.removeChild(a)
21 | })
22 | }
--------------------------------------------------------------------------------
/www/src/api/host.js:
--------------------------------------------------------------------------------
1 | import {GET, POST, PUT, DELETE} from '@/utils/request'
2 |
3 | // 用户相关接口
4 | export const PostNewGroup = (group) => POST('/api/host/group', group)
5 | export const PutGroup = (group) => PUT('/api/host/group', group)
6 | export const GetAllGroup = () => GET('/api/host/group')
7 | export const DelGroup = (groupId) => DELETE('/api/host/group/' + groupId)
8 | export const PostNewHost = (host) => POST('/api/host', host)
9 | export const PutHost = (host) => PUT('/api/host', host)
10 | export const DelHost = (hostId) => DELETE('/api/host/' + hostId)
11 | export const PingHost = (hostId) => GET('/api/host/ping/' + hostId)
12 | export const GetHostById = (hostId) => GET('/api/host/' + hostId)
13 | export const GetHostsByIds = (ids) => GET('/api/host/targets/?'+ ids)
--------------------------------------------------------------------------------
/www/src/api/jobs.js:
--------------------------------------------------------------------------------
1 | import {GET, POST, PUT, DELETE} from '@/utils/request'
2 |
3 | export const PostNewCronJob = (dict) => POST('/api/jobs/', dict)
4 | export const GetJobList = () => GET('/api/jobs/')
5 | export const DelJob = (jobId) => DELETE('/api/jobs/' + jobId)
6 | export const PutCronJob = (job) => PUT('/api/jobs/', job)
7 | export const SwitchJob = (jobId, status) => GET('/api/jobs/switch/' + jobId, {status: status})
8 | export const GetLogs = (jobId) => GET('/api/jobs/logs')
--------------------------------------------------------------------------------
/www/src/api/menus.js:
--------------------------------------------------------------------------------
1 | import { GET, POST, PUT, DELETE } from '@/utils/request'
2 |
3 | // 菜单接口
4 | export const GetAllMenus = () => GET('/api/menus')
5 | export const PostNewMenu = (menu) => POST('/api/menus', menu )
6 | export const PutMenu = (menu) => PUT('/api/menus', menu)
7 | export const DeleteMenu = (id) => DELETE('/api/menus/' + id)
--------------------------------------------------------------------------------
/www/src/api/playbook.js:
--------------------------------------------------------------------------------
1 | import {GET, POST, PUT, DELETE} from '@/utils/request'
2 |
3 | //playbook相关接口
4 | export const GetPlaybook = (playbookId) => GET('/api/playbook/' + playbookId)
5 | export const PostNewPlaybook = (playbook) => POST('/api/playbook', playbook)
6 | export const PutPlaybook = (playbook) => PUT('/api/playbook', playbook)
7 | export const DelPlaybook = (playbookId) => DELETE('/api/playbook/' + playbookId)
8 | export const GetPlaybooksByQuery = (query) => GET('/api/playbook', {query})
--------------------------------------------------------------------------------
/www/src/api/roles.js:
--------------------------------------------------------------------------------
1 | import {GET, POST, PUT, DELETE} from '@/utils/request'
2 |
3 | // 角色接口
4 | export const GetRoles = (q) => GET('/api/roles', {q})
5 | export const GetRoleInfo = (roleId) => GET('/api/roles' + roleId)
6 | export const GetRoleEnableMenus = (roleId) => GET('/api/roles/enable-menus', {id: roleId})
7 | export const PostNewRoles = (role) => POST('/api/roles', role)
8 | export const PutRoles = (role) => PUT('/api/roles', role)
9 | export const DeleteRole = (roleId) => DELETE('/api/roles/' + roleId)
10 | export const GetRoleExist = (name) => GET('/api/roles/exist', {name})
--------------------------------------------------------------------------------
/www/src/api/users.js:
--------------------------------------------------------------------------------
1 | import {GET, POST, PUT, DELETE} from '@/utils/request'
2 |
3 | // 用户相关接口
4 | export const requestLogin = (username, password) => POST('/api/login', {username, password})
5 | export const GetUserInfo = (uid) => GET('/api/users/' + uid)
6 | export const GetUserPermission = () => GET('/api/permission')
7 | export const GetUserExist = (name) => GET('/api/users/exist', {name})
8 | export const GetUserRoles = (userId) => GET('/api/users/roles', {id: userId})
9 | export const PutNewUser = (user, roles) => PUT('/api/users/' + user.id, {user, roles})
10 | export const PostAddUser = (user, roles) => POST('/api/users', {user, roles})
11 | export const DeleteUser = (userId) => DELETE('/api/users/' + userId)
12 | export const ResetPasswd = (userId, password) => PUT('/api/users/password', {id: userId, password})
--------------------------------------------------------------------------------
/www/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/4linuxfun/Fastapi-Admin/5c057ea38a5c8a1c9189ee7090c306979a1dfaf6/www/src/assets/logo.png
--------------------------------------------------------------------------------
/www/src/autoImport.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by 'unplugin-auto-import'
2 | export {}
3 | declare global {
4 | const EffectScope: typeof import('vue')['EffectScope']
5 | const computed: typeof import('vue')['computed']
6 | const createApp: typeof import('vue')['createApp']
7 | const customRef: typeof import('vue')['customRef']
8 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
9 | const defineComponent: typeof import('vue')['defineComponent']
10 | const effectScope: typeof import('vue')['effectScope']
11 | const getCurrentInstance: typeof import('vue')['getCurrentInstance']
12 | const getCurrentScope: typeof import('vue')['getCurrentScope']
13 | const h: typeof import('vue')['h']
14 | const inject: typeof import('vue')['inject']
15 | const isProxy: typeof import('vue')['isProxy']
16 | const isReactive: typeof import('vue')['isReactive']
17 | const isReadonly: typeof import('vue')['isReadonly']
18 | const isRef: typeof import('vue')['isRef']
19 | const markRaw: typeof import('vue')['markRaw']
20 | const nextTick: typeof import('vue')['nextTick']
21 | const onActivated: typeof import('vue')['onActivated']
22 | const onBeforeMount: typeof import('vue')['onBeforeMount']
23 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
24 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
25 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
26 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
27 | const onDeactivated: typeof import('vue')['onDeactivated']
28 | const onErrorCaptured: typeof import('vue')['onErrorCaptured']
29 | const onMounted: typeof import('vue')['onMounted']
30 | const onRenderTracked: typeof import('vue')['onRenderTracked']
31 | const onRenderTriggered: typeof import('vue')['onRenderTriggered']
32 | const onScopeDispose: typeof import('vue')['onScopeDispose']
33 | const onServerPrefetch: typeof import('vue')['onServerPrefetch']
34 | const onUnmounted: typeof import('vue')['onUnmounted']
35 | const onUpdated: typeof import('vue')['onUpdated']
36 | const provide: typeof import('vue')['provide']
37 | const reactive: typeof import('vue')['reactive']
38 | const readonly: typeof import('vue')['readonly']
39 | const ref: typeof import('vue')['ref']
40 | const resolveComponent: typeof import('vue')['resolveComponent']
41 | const resolveDirective: typeof import('vue')['resolveDirective']
42 | const shallowReactive: typeof import('vue')['shallowReactive']
43 | const shallowReadonly: typeof import('vue')['shallowReadonly']
44 | const shallowRef: typeof import('vue')['shallowRef']
45 | const toRaw: typeof import('vue')['toRaw']
46 | const toRef: typeof import('vue')['toRef']
47 | const toRefs: typeof import('vue')['toRefs']
48 | const triggerRef: typeof import('vue')['triggerRef']
49 | const unref: typeof import('vue')['unref']
50 | const useAttrs: typeof import('vue')['useAttrs']
51 | const useCssModule: typeof import('vue')['useCssModule']
52 | const useCssVars: typeof import('vue')['useCssVars']
53 | const useLink: typeof import('vue-router')['useLink']
54 | const useRoute: typeof import('vue-router')['useRoute']
55 | const useRouter: typeof import('vue-router')['useRouter']
56 | const useSlots: typeof import('vue')['useSlots']
57 | const watch: typeof import('vue')['watch']
58 | const watchEffect: typeof import('vue')['watchEffect']
59 | const watchPostEffect: typeof import('vue')['watchPostEffect']
60 | const watchSyncEffect: typeof import('vue')['watchSyncEffect']
61 | }
62 |
--------------------------------------------------------------------------------
/www/src/components.d.ts:
--------------------------------------------------------------------------------
1 | // generated by unplugin-vue-components
2 | // We suggest you to commit this file into source control
3 | // Read more: https://github.com/vuejs/core/pull/3399
4 | import '@vue/runtime-core'
5 |
6 | export {}
7 |
8 | declare module '@vue/runtime-core' {
9 | export interface GlobalComponents {
10 | AutoDict: typeof import('./components/AutoDict.vue')['default']
11 | AutoFormDialog: typeof import('./components/AutoFormDialog.vue')['default']
12 | ElAside: typeof import('element-plus/es')['ElAside']
13 | ElAvatar: typeof import('element-plus/es')['ElAvatar']
14 | ElCol: typeof import('element-plus/es')['ElCol']
15 | ElContainer: typeof import('element-plus/es')['ElContainer']
16 | ElDropdown: typeof import('element-plus/es')['ElDropdown']
17 | ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
18 | ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
19 | ElHeader: typeof import('element-plus/es')['ElHeader']
20 | ElIcon: typeof import('element-plus/es')['ElIcon']
21 | ElMain: typeof import('element-plus/es')['ElMain']
22 | ElMenu: typeof import('element-plus/es')['ElMenu']
23 | ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
24 | ElRow: typeof import('element-plus/es')['ElRow']
25 | ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
26 | ElTabPane: typeof import('element-plus/es')['ElTabPane']
27 | ElTabs: typeof import('element-plus/es')['ElTabs']
28 | HeaderContent: typeof import('./components/HeaderContent.vue')['default']
29 | InputPlus: typeof import('./components/InputPlus.vue')['default']
30 | MenuList: typeof import('./components/MenuList.vue')['default']
31 | ResetButton: typeof import('./components/ResetButton.vue')['default']
32 | Roles: typeof import('./components/roles.vue')['default']
33 | RouterLink: typeof import('vue-router')['RouterLink']
34 | RouterView: typeof import('vue-router')['RouterView']
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/www/src/components/AutoDict.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 | 全选
22 |
23 |
24 |
25 | {{ item.label }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
100 |
101 |
--------------------------------------------------------------------------------
/www/src/components/AutoFormDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 取消
16 | 提交
17 |
18 |
19 |
20 |
21 |
121 |
122 |
124 |
--------------------------------------------------------------------------------
/www/src/components/HeaderContent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {{ userName }}
17 |
18 |
19 | 个人中心
20 | 退出
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
75 |
78 |
--------------------------------------------------------------------------------
/www/src/components/InputPlus.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
9 |
10 |
12 |
13 |
14 |
25 |
26 |
28 |
--------------------------------------------------------------------------------
/www/src/components/MenuList.vue:
--------------------------------------------------------------------------------
1 |
2 |
44 |
45 |
46 |
67 |
68 |
--------------------------------------------------------------------------------
/www/src/components/ResetButton.vue:
--------------------------------------------------------------------------------
1 |
2 | 重置
3 |
4 |
5 |
18 |
19 |
--------------------------------------------------------------------------------
/www/src/components/roles.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{scope.row.enable === 1?'启用':'禁用'}}
10 |
11 |
12 |
13 |
14 | 编辑权限
15 | 删除
16 |
17 |
18 |
19 |
20 |
43 |
--------------------------------------------------------------------------------
/www/src/composables/useMenu.js:
--------------------------------------------------------------------------------
1 | import {reactive, ref, watch, computed} from 'vue'
2 |
3 | export default function (form, menuData, emit) {
4 | const selectData = reactive(form)
5 |
6 | watch(selectData, (newData) => {
7 | console.log('watch selectData change:' + selectData)
8 | emit('update:form', selectData)
9 | })
10 |
11 |
12 | const menuMap = (arr) => {
13 | const menu = arr.filter(item => item.type !== 'btn')
14 | for (let item of menu) {
15 | if (item.children && item.children.length > 0) {
16 | item.children = menuMap(item.children)
17 | }
18 | }
19 | return menu
20 | }
21 | //cascaderMenu变量不包含menuData变量中type为btn的值,如果children属性值包含内容,则再进行遍历判断
22 | const cascaderMenu = computed(() => {
23 | let menu = menuMap(menuData)
24 | console.log(menu)
25 | return menu
26 | })
27 |
28 | return {
29 | selectData,
30 | cascaderMenu
31 | }
32 | }
--------------------------------------------------------------------------------
/www/src/composables/usePagination.js:
--------------------------------------------------------------------------------
1 | import {reactive, ref, watch} from 'vue'
2 | import {POST} from '@/utils/request'
3 |
4 | /**
5 | * 分页逻辑复用函数
6 | * @param {string} url - 请求的API地址
7 | * @param {Object} searchForm - 搜索表单数据,默认为空对象
8 | * @param {string} orderType - 排序方式,默认为 'asc'
9 | * @param {number} initialPageSize - 每页显示条数,默认为 10
10 | * @returns {Object} - 返回分页相关的状态和方法
11 | */
12 | export default function usePagination(url, searchForm = {}, orderType = 'asc', initialPageSize = 10) {
13 | // 响应式搜索表单数据
14 | const search = reactive(searchForm)
15 |
16 | // 表格数据
17 | const tableData = ref([])
18 |
19 | // 每页显示条数
20 | const pageSize = ref(initialPageSize)
21 |
22 | // 当前页码
23 | const currentPage = ref(1)
24 |
25 | // 排序方式
26 | const orderModel = ref(orderType)
27 |
28 | // 数据总数
29 | const total = ref(0)
30 |
31 | /**
32 | * 获取分页数据
33 | * @returns {Promise}
34 | */
35 | const fetchData = async () => {
36 | const response = await POST(url, {
37 | search,
38 | page: currentPage.value,
39 | page_size: pageSize.value,
40 | model: orderModel.value,
41 | })
42 | tableData.value = response.data
43 | total.value = response.total
44 | }
45 |
46 | /**
47 | * 刷新当前页数据
48 | */
49 | const freshCurrentPage = async () => {
50 | await fetchData()
51 | }
52 |
53 | /**
54 | * 处理搜索操作,重置页码并获取数据
55 | */
56 | const handleSearch = async () => {
57 | currentPage.value = 1
58 | await fetchData()
59 | }
60 |
61 | // 监听 currentPage 和 pageSize 的变化,自动获取数据
62 | watch([currentPage, pageSize], async () => {
63 | await fetchData()
64 | })
65 |
66 | // 返回分页相关的状态和方法
67 | return {
68 | search,
69 | tableData,
70 | currentPage,
71 | pageSize,
72 | orderModel,
73 | total,
74 | freshCurrentPage,
75 | handleSearch,
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/www/src/composables/useTerm.js:
--------------------------------------------------------------------------------
1 | import {onMounted, ref, watch, onBeforeUnmount, nextTick} from 'vue'
2 | import {FitAddon} from 'xterm-addon-fit'
3 | import {Terminal} from 'xterm'
4 | import 'xterm/css/xterm.css'
5 |
6 | /**
7 | * @name: useTerm,用于统一使用terminal
8 | * @description: 终端
9 | * @return {Promise}
10 | *
11 | * 示例用法:
12 | * 创建终端
13 | *
14 | * const terminalRef = ref(null)
15 | * const {term, initTerm} = useTerm(terminalRef)
16 | * 手动初始化
17 | * await initTerm()
18 | * 写入内容
19 | * term.value.write('写入terminal 内容\n')
20 | */
21 | export default function useTerm(terminalRef) {
22 | const term = ref(null)
23 | const fitAddon = new FitAddon()
24 |
25 | async function initTerm() {
26 | if(!terminalRef.value){
27 | console.error('Terminal container not found')
28 | return
29 | }
30 | console.log('init terminal')
31 | term.value = new Terminal({
32 | rendererType: 'canvas',
33 | disableStdin: false,
34 | convertEol: true,
35 | cursorStyle: 'block',
36 | scrollback: 9999999,
37 | })
38 | term.value.open(terminalRef.value)
39 | term.value.loadAddon(fitAddon)
40 |
41 | await nextTick(async () => {
42 | fitAddon.fit()
43 | console.log('fit ok')
44 | })
45 |
46 | term.value.focus()
47 | console.log('init terminal ok')
48 | }
49 |
50 | const handleResize = () => {
51 | fitAddon.fit()
52 | }
53 |
54 | window.addEventListener('resize', handleResize)
55 |
56 | onBeforeUnmount(() => {
57 | window.removeEventListener('resize', handleResize)
58 | })
59 |
60 | return {
61 | term,
62 | initTerm
63 | }
64 | }
--------------------------------------------------------------------------------
/www/src/main.js:
--------------------------------------------------------------------------------
1 | import {createApp} from 'vue'
2 | import App from './App.vue'
3 | import ElementPlus from 'element-plus'
4 | import * as ElementPlusIconsVue from '@element-plus/icons-vue'
5 | import zhCn from 'element-plus/dist/locale/zh-cn'
6 | import 'element-plus/dist/index.css'
7 | import router from './router'
8 | import {createPinia} from 'pinia'
9 | import {useStore} from './stores'
10 | import './permission'
11 | import 'uno.css'
12 |
13 |
14 | const app = createApp(App)
15 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
16 | app.component(key, component)
17 | }
18 |
19 | app.use(ElementPlus, {locale: zhCn})
20 | app.use(router)
21 | app.use(createPinia())
22 |
23 | app.directive('permission', {
24 | mounted(el, binding) {
25 | console.log('permission run')
26 | console.log(el, binding)
27 | const store = useStore()
28 | let permission = binding.value
29 | console.log('check permission:' + permission)
30 | let btnPermissions = store.buttons
31 | console.log(btnPermissions)
32 | if (!btnPermissions.includes(permission)) {
33 | el.parentNode.removeChild(el)
34 | }
35 | }
36 | })
37 | app.mount('#app')
--------------------------------------------------------------------------------
/www/src/permission.js:
--------------------------------------------------------------------------------
1 | import router from './router'
2 | import {useStore} from './stores'
3 | import {getToken} from '@/utils/auth'
4 | import {makeRouter} from '@/utils/router'
5 | import {useTabsStore} from '@/stores/tabs'
6 |
7 | const whiteList = ['/login'] // no redirect whitelist
8 |
9 | router.beforeEach(async (to) => {
10 | const store = useStore()
11 | const {tabAdd} = useTabsStore()
12 | console.log('start before each', to)
13 |
14 | if (getToken()) {
15 | console.log('已经有token')
16 | // 已登录且要跳转的页面是登录页
17 | if (to.path === '/login') {
18 | return '/'
19 | } else {
20 | console.log('已经登录成功')
21 | //登录成功,需要判断router是不是已经按照权限要求构建好,并且菜单是否按照权限要求生成,如没有,则生成
22 | // router.push('/')
23 | if (store.asyncRoutes.length === 0) {
24 | console.log('asyncroutes is not set')
25 | try {
26 | await store.getInfo()
27 | await store.getPermission()
28 | let asyncRoutes = await makeRouter(store.asyncRoutes)
29 | console.log(asyncRoutes)
30 | for (let route of asyncRoutes) {
31 | console.log('add route:')
32 | console.log(route)
33 | // 判断route是否有children,如果没有,则增加到'/'的children中
34 | if (!route.children || route.children.length === 0) {
35 | console.log('no children')
36 | router.addRoute('home', route)
37 | } else {
38 | router.addRoute(route)
39 | }
40 | }
41 | return {path: to.fullPath, replace: true}
42 | } catch (e) {
43 | console.log('用户权限拉取失败' + e)
44 | await store.logOut()
45 | location.reload()
46 | }
47 | }
48 | console.log('当前生效路由表')
49 | console.log(router.getRoutes())
50 | tabAdd(to)
51 | return true
52 | }
53 | } else {
54 | // 无token信息,表示未登录
55 | console.log('无token信息')
56 | if (whiteList.indexOf(to.path) !== -1) {
57 | // 在免登录白名单,直接进入
58 | return true
59 | } else {
60 | // return true
61 | router.replace(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
62 | }
63 | }
64 | })
65 |
--------------------------------------------------------------------------------
/www/src/router/index.js:
--------------------------------------------------------------------------------
1 | import {createRouter, createWebHistory} from 'vue-router'
2 |
3 |
4 | export const constantRouterMap = [
5 | {
6 | path: '/',
7 | name: 'home',
8 | component: () => import('@/views/Layout'),
9 | children: [
10 | {
11 | path: '',
12 | name: '首页',
13 | component: () => import('@/views/DashBoard')
14 | },
15 | {
16 | path: '404',
17 | name: '404',
18 | component: () => import('@/views/errorPage/NotFound'),
19 | },
20 | {
21 | path: ':pathMatch(.*)*',
22 | name: '404',
23 | component: () => import('@/views/errorPage/NotFound'),
24 | },
25 | ]
26 | },
27 | {
28 | path: '/login',
29 | component: () => import('@/views/Login/index'),
30 | // hidden: true
31 | },
32 | ]
33 |
34 | export default createRouter({
35 | history: createWebHistory(),
36 | // scrollBehavior: () => ({ y: 0 }),
37 | routes: constantRouterMap
38 | })
--------------------------------------------------------------------------------
/www/src/stores/collapse.js:
--------------------------------------------------------------------------------
1 | import {defineStore} from 'pinia'
2 | import {ref} from 'vue'
3 |
4 | export const useCollapseStore = defineStore('collapse', () => {
5 | const collapse = ref(false)
6 | return {collapse}
7 | })
--------------------------------------------------------------------------------
/www/src/stores/dict.js:
--------------------------------------------------------------------------------
1 | import {defineStore} from "pinia";
2 | import {computed, ref} from 'vue'
3 | import {GetDictItems} from '@/api/dictonary'
4 |
5 | export const useDictStore = defineStore('dict', () => {
6 | const dictObject = ref({})
7 | // 通过dict,返回对应的值.构建一个带参数的getter
8 | const dictItems = computed(() => {
9 | return (dictName) => {
10 | if (!dictObject.value.hasOwnProperty(dictName)) {
11 | GetDictItems(dictName).then(response => {
12 | dictObject.value[dictName] = response
13 | })
14 | }
15 | return dictObject.value[dictName]
16 |
17 | }
18 | })
19 |
20 | async function getDictItems(dictName) {
21 | if (!dictObject.value.hasOwnProperty(dictName)) {
22 | console.log('dictObject has no dict:'+dictName)
23 | dictObject.value[dictName] = await GetDictItems(dictName)
24 | }
25 | return dictObject.value[dictName]
26 | }
27 |
28 | return {dictObject, dictItems, getDictItems}
29 | })
--------------------------------------------------------------------------------
/www/src/stores/index.js:
--------------------------------------------------------------------------------
1 | import {defineStore} from 'pinia'
2 | import {ref} from 'vue'
3 | import {
4 | requestLogin,
5 | GetUserInfo,
6 | GetUserPermission
7 | } from '@/api/users'
8 | import {
9 | setToken,
10 | removeToken
11 | } from '@/utils/auth'
12 |
13 | export const useStore = defineStore('user', () => {
14 |
15 | const uid = ref('')
16 | const token = ref('')
17 | const name = ref(null)
18 | const avatar = ref(null)
19 | const asyncRoutes = ref([])
20 | const buttons = ref([])
21 |
22 | //执行登录请求,获取token
23 | function logIn(userInfo) {
24 | const username = userInfo.username
25 | const password = userInfo.password
26 | const rememberMe = userInfo.rememberMe
27 | console.log('user login actions')
28 | return new Promise((resolve, reject) => {
29 | requestLogin(username, password).then((response) => {
30 | console.log(response)
31 | setToken(response.token, rememberMe)
32 | token.value = response.token
33 | uid.value = response.uid
34 | resolve()
35 | }).catch((error) => {
36 | reject(error)
37 | })
38 | })
39 | }
40 |
41 | // 获取用户状态信息
42 | function getInfo() {
43 | return new Promise((resolve, reject) => {
44 | console.log('get user info')
45 | GetUserInfo(this.uid).then(response => {
46 | name.value = response.name
47 | avatar.value = response.avatar
48 | resolve(response)
49 | }).catch(error => {
50 | reject(error)
51 | })
52 | })
53 | }
54 |
55 | function logOut() {
56 | return new Promise((resolve) => {
57 | // this.$reset()
58 | removeToken()
59 | resolve()
60 | })
61 | }
62 |
63 | // 获取用户权限列表
64 | function getPermission() {
65 | return new Promise((resolve, reject) => {
66 | GetUserPermission().then(response => {
67 | console.log('permission response is:')
68 | console.log(response)
69 | asyncRoutes.value = (response.menus)
70 | buttons.value = (response.btns)
71 | resolve(response)
72 | }).catch(error => {
73 | reject(error)
74 | })
75 | })
76 | }
77 |
78 | return {uid, token, name, avatar, asyncRoutes, buttons, getInfo, logIn, logOut, getPermission}
79 | }
80 | )
--------------------------------------------------------------------------------
/www/src/stores/tabs.js:
--------------------------------------------------------------------------------
1 | import {defineStore} from 'pinia'
2 | import {computed, nextTick, ref} from 'vue'
3 | import router from '@/router'
4 |
5 | export const useTabsStore = defineStore('tabs', () => {
6 | const allTabs = ref([{name: '首页', path: '/'},])
7 | const currentTab = ref(null)
8 | const cacheTabs = ref([])
9 |
10 | const tabsList = computed(() => {
11 | return allTabs
12 | })
13 |
14 |
15 | // 打开新tab时执行
16 | function tabAdd(to) {
17 | const isExist = allTabs.value.some(item => {
18 | return item.path === to.fullPath
19 | })
20 | if (!isExist) {
21 | allTabs.value.push({
22 | name: to.name,
23 | path: to.fullPath
24 | })
25 | cacheTabs.value.push(to.name)
26 | }
27 | currentTab.value = to.name
28 | }
29 |
30 | function tabRemove(removeTab) {
31 | console.log('tab remove:', removeTab)
32 | for (let i = 0; i < allTabs.value.length; i++) {
33 | if (allTabs.value[i].name === removeTab) {
34 | allTabs.value.splice(i, 1)
35 | cacheTabs.value.splice(i, 1)
36 | const nextId = i === allTabs.value.length ? i - 1 : i
37 | currentTab.value = allTabs.value[nextId].name
38 | console.log(currentTab.value)
39 | router.push(allTabs.value[nextId].path)
40 | }
41 | }
42 | }
43 |
44 | function tabClick(selectTab) {
45 | console.log('tab click', selectTab)
46 | currentTab.value = selectTab.paneName
47 | console.log('current tab value:', currentTab.value)
48 | console.log('all tabs:', allTabs)
49 | allTabs.value.forEach(tab => {
50 | console.log(tab.name)
51 | if (tab.name === currentTab.value) {
52 | console.log('router to:', tab.path)
53 | router.push(tab.path)
54 | }
55 | })
56 | }
57 |
58 | return {currentTab, allTabs, cacheTabs, tabsList, tabAdd, tabRemove, tabClick}
59 | })
--------------------------------------------------------------------------------
/www/src/utils/auth.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const TokenKey = 'Token'
4 |
5 | export function getToken() {
6 | return Cookies.get(TokenKey)
7 | }
8 |
9 | export function setToken(token, rememberMe) {
10 | if (rememberMe) {
11 | return Cookies.set(TokenKey, token, { expires: 1 })
12 | } else {
13 | return Cookies.set(TokenKey, token)
14 | }
15 | }
16 |
17 | export function removeToken() {
18 | return Cookies.remove(TokenKey)
19 | }
--------------------------------------------------------------------------------
/www/src/utils/common.js:
--------------------------------------------------------------------------------
1 | // 一些通用的js
2 | /**
3 | * 将树形数据扁平化为 { id: node } 的形式
4 | * @param {Array} nodes - 树形数据
5 | * @param {Object} props - 属性配置,例如:{ id: 'id', children: 'children' }
6 | * @returns {Map} - 返回一个 Map,key 是节点的 id,value 是节点本身
7 | */
8 | function flattenTree(nodes, props = {}) {
9 | const {id = 'id', children = 'children'} = props
10 | const nodeMap = new Map()
11 |
12 | function flatten(nodes) {
13 | nodes.forEach(node => {
14 | nodeMap.set(node[id], node) // 将节点存入 Map
15 | if (node[children] && node[children].length > 0) {
16 | flatten(node[children]) // 递归处理子节点
17 | }
18 | })
19 | }
20 |
21 | flatten(nodes)
22 | return nodeMap
23 | }
24 |
25 | /**
26 | * 通过 id 查找节点
27 | * @param {Map} nodeMap - 扁平化的节点 Map
28 | * @param {number|string} targetId - 目标节点的 id
29 | * @returns {Object|null} - 返回找到的节点,如果未找到则返回 null
30 | */
31 | export function findNodeById(nodeMap, targetId) {
32 | return nodeMap.get(targetId) || null
33 | }
34 |
35 |
36 | /**
37 | * 通过id生成对应的层级label,例如:根节点/子节点/孙节点
38 | * @param {Array} data - 传入的树形数据, 例如:[{id: 1, name: '根节点', children: [{id: 2, name: '子节点', children: [{id: 3, name: '孙节点'}]}]}]
39 | * @param {number} findId - 传入的唯一值
40 | * @param {Object} props - 传入的属性,例如:{id: 'id', children: 'children', label: 'label',parent_id: 'parent_id'}
41 | * @returns {string} - 返回层级label,例如:根节点/子节点/孙节点
42 | */
43 | export function getHierarchyLabel(data, findId, props) {
44 | const defaultProps = {
45 | id: 'id',
46 | children: 'children',
47 | label: 'label',
48 | parent_id: 'parent_id'
49 | }
50 | const id = props && props.hasOwnProperty('id') ? props.id : defaultProps.id
51 | const children = props && props.hasOwnProperty('children') ? props.children : defaultProps.children
52 | const label = props && props.hasOwnProperty('label') ? props.label : defaultProps.label
53 | const parent_id = props && props.hasOwnProperty('parent_id') ? props.parent_id : defaultProps.parent_id
54 |
55 | let nodeMap = flattenTree(data, {id: 'id', children: 'children'})
56 | let hierarchyLabel = ''
57 | while (true) {
58 | let selectNode = findNodeById(nodeMap, findId)
59 | hierarchyLabel = hierarchyLabel ? selectNode[label] + '/' + hierarchyLabel : selectNode[label]
60 | if (selectNode[parent_id] === null) {
61 | hierarchyLabel = '/' + hierarchyLabel
62 | break
63 | }
64 | findId = selectNode[parent_id]
65 | }
66 | console.log(hierarchyLabel)
67 | return hierarchyLabel
68 | }
69 |
70 |
71 | /**
72 | * 给[{},{}]类型的数据递归增加自定义属性值,并返回新数组
73 | * @param {Array} data - 传入的树形数据, 例如:[{id: 1, name: '根节点', children: [{id: 2, name: '子节点', children: [{id: 3, name: '孙节点'}]}]}]
74 | * @param {string} customProperty - 传入的自定义属性名称, 例如:'isExpanded'
75 | * @param value - 自定义的值
76 | * @returns {Array} - 返回新数组
77 | */
78 | export function addCustomProperty(data, customProperty, value) {
79 | return data.map(item => {
80 | item[customProperty] = value
81 | if (item.children) {
82 | item.children = addCustomProperty(item.children, customProperty, value)
83 | }
84 | return item
85 | })
86 | }
87 |
88 | /**
89 | * 给[{},{}]类型的数据递归查找某个属性,修改值,并返回新数组
90 | * @param {Array} data - 传入的树形数据, 例如:[{id: 1, name: '根节点', children: [{id: 2, name: '子节点', children: [{id: 3, name: '孙节点'}]}]}]
91 | * @param {string} customProperty - 传入的自定义属性名称, 例如:'isExpanded'
92 | * @param value - 自定义的值
93 | * @param newValue - 新的值
94 | * @returns {Array} - 返回新数组
95 | */
96 | export function updateCustomProperty(data, customProperty, value, newValue) {
97 | return data.map(item => {
98 | if (item[customProperty] === value) {
99 | item[customProperty] = newValue
100 | }
101 | if (item.children) {
102 | item.children = updateCustomProperty(item.children, customProperty, value, newValue)
103 | }
104 | return item
105 | })
106 | }
--------------------------------------------------------------------------------
/www/src/utils/router.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/views/Layout'
2 | import {shallowRef} from 'vue'
3 |
4 | // 通过服务端传回来的权限菜单列表,添加部分字段
5 | export function makeRouter(routers) { // 遍历后台传来的路由字符串,转换为组件对象
6 | console.log(routers)
7 | let asyncRouter = routers.map(route => {
8 | console.log('name:' + route.name + ' path:' + route.path)
9 | if (route.component) {
10 | route.component = loadView(route.component)
11 | } else {
12 | route.component = null
13 | }
14 |
15 | if (route.children && route.children.length) {
16 | route.children = makeRouter(route.children)
17 | }
18 | return route
19 | })
20 | console.log('asyncrouter is:', asyncRouter) // 修改: 更改为对象输出
21 | return asyncRouter
22 | }
23 |
24 | export const loadView = (view) => { // 路由懒加载
25 | return () => import(`@/views/${view}`)
26 | }
27 |
--------------------------------------------------------------------------------
/www/src/views/DashBoard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
76 |
77 |
--------------------------------------------------------------------------------
/www/src/views/Home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 登录成功,token为{{token}}
4 |
5 |
6 |
18 |
--------------------------------------------------------------------------------
/www/src/views/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
52 |
53 |
--------------------------------------------------------------------------------
/www/src/views/PersonalCenter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 个人中心
4 |
5 |
6 |
11 |
--------------------------------------------------------------------------------
/www/src/views/errorPage/401.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 401
4 |
5 |
6 |
11 |
--------------------------------------------------------------------------------
/www/src/views/errorPage/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
404找不到页面
4 |
5 |
6 |
11 |
--------------------------------------------------------------------------------
/www/src/views/errorPage/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
404
5 |
抱歉,您访问的页面不存在!
6 |
7 |
8 |
9 |
10 |
12 |
13 |
35 |
--------------------------------------------------------------------------------
/www/src/views/host/AddGroup.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 | {{ getHierarchyLabel(allGroups, value, {label: 'name'}) }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 提交
25 | 取消
26 |
27 |
28 |
29 |
30 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/www/src/views/host/HostDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 | {{ getHierarchyLabel(allGroups, value, {label: 'name'}) }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ssh
22 |
23 |
24 |
25 |
26 | @
27 |
28 |
29 |
30 |
31 | -p
32 |
33 |
34 |
35 |
36 |
37 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 取消
46 | 保存
47 | 验证
48 |
49 |
50 |
51 |
52 |
137 |
138 |
--------------------------------------------------------------------------------
/www/src/views/jobs/Execute/LogDrawer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/www/src/views/jobs/Execute/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 查看日志
16 |
17 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
36 |
37 |
76 |
--------------------------------------------------------------------------------
/www/src/views/jobs/JobManage/JobLogs.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ props.job.id }}
4 | {{ props.job.name }}
5 | {{ props.job.trigger }}
6 |
7 | 模块:{{
9 | props.job.ansible_args.module
10 | }},参数:{{ props.job.ansible_args.module_args }}
11 | 脚本:{{ props.job.ansible_args.playbook }}
12 |
13 |
14 | {{ props.job.trigger_args.cron }}
15 | {{ props.job.trigger_args.start_date }}
16 | {{ props.job.trigger_args.end_date }}
17 |
18 |
19 | {{ props.job.trigger_args.run_date }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {{ statsCheck(scope.row.stats) ? '成功' : '失败' }}
30 |
31 |
32 |
33 |
34 |
35 | 详情
36 |
37 |
38 |
39 |
40 |
41 |
42 |
46 |
47 |
48 |
98 |
99 |
--------------------------------------------------------------------------------
/www/src/views/jobs/JobManage/LogDetail.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | {{ taskLogInfo.start_time }}
9 | {{ taskLogInfo.end_time }}
10 |
11 |
12 |
13 |
14 | {{ host }}
15 | {{ num }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{ host }}
25 | {{ num }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
80 |
81 |
--------------------------------------------------------------------------------
/www/src/views/jobs/playbook/AddPlaybook.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 取消
19 | 保存
20 |
21 |
22 |
23 |
24 |
101 |
--------------------------------------------------------------------------------
/www/src/views/jobs/playbook/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 模板列表
15 |
16 |
17 | 新建模板
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 编辑
28 | 复制
29 | 删除
30 |
31 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
48 |
49 |
88 |
89 |
--------------------------------------------------------------------------------
/www/src/views/system/dictonary/AddDict.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 关闭
15 |
17 | 确定
18 |
19 |
20 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/www/src/views/system/dictonary/AddItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 关闭
22 | 确定
23 |
24 |
25 |
26 |
27 |
28 |
120 |
121 |
--------------------------------------------------------------------------------
/www/src/views/system/dictonary/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 查询
12 |
13 |
14 |
15 |
16 | 重置
17 |
18 |
19 |
20 |
21 |
22 | 添加
23 | 导出
24 | 导入
25 | 刷新缓存
26 | 回收站
27 |
28 |
29 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 操作
38 |
39 |
40 | 字典配置
41 |
42 |
43 | 删除
44 |
45 |
46 |
47 |
48 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
157 |
158 |
--------------------------------------------------------------------------------
/www/src/views/system/menus/ButtonForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
33 |
34 |
--------------------------------------------------------------------------------
/www/src/views/system/menus/MenuDrawer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 | 一级菜单:菜单路径‘/’开头,前端组件为Layout,如果一级菜单没子菜单,则前端组件为对应组件名
8 | 二级菜单:菜单路径为子路由path,前端组件为对应的组件名
9 |
10 |
11 |
12 |
13 |
14 | 一级菜单
15 | 子菜单
16 | 按钮
17 |
18 |
19 |
20 |
21 | 取消
22 |
23 | 更新
24 | 添加
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
104 |
105 |
107 |
--------------------------------------------------------------------------------
/www/src/views/system/roles/RoleDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 | {{ data.name }}
22 |
23 |
24 |
25 |
26 | 取消
27 | 确定
28 |
29 |
30 |
31 |
32 |
33 |
150 |
151 |
157 |
--------------------------------------------------------------------------------
/www/src/views/system/roles/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 | 搜索
15 |
16 | 重置
17 | 添加新角色
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ scope.row.enable === true ? '启用' : '禁用' }}
31 |
32 |
33 |
34 |
35 |
36 | 编辑
38 |
39 | 删除
41 |
42 |
43 |
44 |
45 |
46 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
136 |
--------------------------------------------------------------------------------
/www/src/views/system/setting/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 安全设置111222
4 | 密钥设置222
5 | 推送服务设置
6 | 报警服务设置
7 | 开放服务设置
8 |
9 |
10 |
11 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/www/src/views/system/user/ResetPasswdDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 关闭
15 | 确定
16 |
17 |
18 |
19 |
86 |
87 |
--------------------------------------------------------------------------------
/www/src/views/system/user/UserDrawer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 | 取消
18 | 更新
19 | 添加
20 |
21 |
22 |
23 |
24 |
25 |
128 |
129 |
131 |
--------------------------------------------------------------------------------
/www/vite.config.js:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vite'
2 | import AutoImport from 'unplugin-auto-import/vite'
3 | import Components from 'unplugin-vue-components/vite'
4 | import {ElementPlusResolver} from 'unplugin-vue-components/resolvers'
5 | import {resolve} from 'path'
6 | import vue from '@vitejs/plugin-vue'
7 | import dynamicImport from 'vite-plugin-dynamic-import'
8 | import Unocss from 'unocss/vite'
9 | import {
10 | presetAttributify,
11 | presetIcons,
12 | presetUno,
13 | transformerDirectives,
14 | transformerVariantGroup,
15 | } from 'unocss'
16 |
17 | // https://vitejs.dev/config/
18 | export default defineConfig({
19 | publicDir: 'public',
20 | base: '/',
21 | plugins: [
22 | vue(),
23 | dynamicImport(),
24 | AutoImport({resolvers: [ElementPlusResolver()]}),
25 | Components({
26 | resolvers: [
27 | ElementPlusResolver({
28 | importStyle: 'sass',
29 | }),
30 | ],
31 | }),
32 | Unocss({
33 | presets: [
34 | presetUno(),
35 | presetAttributify(),
36 | presetIcons({
37 | scale: 1.2,
38 | warn: true,
39 | }),
40 | ],
41 | transformers: [
42 | transformerDirectives(),
43 | transformerVariantGroup(),
44 | ]
45 | })
46 | ],
47 | server: {
48 | port: 8080,
49 | host: true,
50 | open: '/',
51 | proxy: {},
52 | },
53 | resolve: {
54 | extensions: ['.vue', '.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
55 | alias: [
56 | {find: '@', replacement: resolve(__dirname, 'src')},
57 | {find: '~/', replacement: resolve(__dirname, 'src/')}
58 | ],
59 | },
60 | build: {
61 | outDir: 'dist',
62 | minify: 'terser',
63 | terserOptions: {
64 | compress: {
65 | //发布关闭调试模式
66 | drop_console: true,
67 | drop_debugger: true,
68 | },
69 | },
70 | },
71 | css: {}
72 |
73 | })
--------------------------------------------------------------------------------