From f2966d0f5c2fa612a1da8491a25a31a35a32bdc1 Mon Sep 17 00:00:00 2001 From: Harry-zklcdc Date: Sun, 29 Dec 2024 01:02:31 +0800 Subject: [PATCH] =?UTF-8?q?[Init]=20=F0=9F=8E=89=20MEGREZ=20Community?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 + .vscode/extensions.json | 3 + .vscode/settings.json | 23 + LICENSE | 661 +++ config.example.yml | 12 + docker-compose.example.yml | 41 + frontend/.editorconfig | 13 + frontend/.eslintrc.cjs | 23 + frontend/.gitignore | 14 + frontend/.prettierrc.json | 10 + frontend/index.html | 16 + frontend/jsconfig.json | 13 + frontend/package-lock.json | 5113 +++++++++++++++++ frontend/package.json | 38 + frontend/postcss.config.js | 6 + frontend/public/favicon.ico | Bin 0 -> 9440 bytes frontend/src/App.vue | 8 + frontend/src/api.js | 126 + frontend/src/assets/layout/_core.scss | 23 + frontend/src/assets/layout/_footer.scss | 8 + frontend/src/assets/layout/_main.scss | 16 + frontend/src/assets/layout/_menu.scss | 160 + frontend/src/assets/layout/_mixins.scss | 15 + frontend/src/assets/layout/_preloading.scss | 47 + frontend/src/assets/layout/_responsive.scss | 110 + frontend/src/assets/layout/_topbar.scss | 204 + frontend/src/assets/layout/_typography.scss | 68 + frontend/src/assets/layout/_utils.scss | 25 + frontend/src/assets/layout/layout.scss | 13 + .../src/assets/layout/variables/_common.scss | 20 + .../src/assets/layout/variables/_dark.scss | 5 + .../src/assets/layout/variables/_light.scss | 5 + frontend/src/assets/logo-text.webp | Bin 0 -> 56630 bytes frontend/src/assets/logo.svg | 18 + frontend/src/assets/styles.scss | 4 + frontend/src/assets/tailwind.css | 3 + frontend/src/components/CopyIcon.vue | 50 + frontend/src/components/EditLabel.vue | 33 + .../src/components/FloatingConfigurator.vue | 12 + frontend/src/components/SectionBanner.vue | 28 + frontend/src/layout/AppFooter.vue | 10 + frontend/src/layout/AppLayout.vue | 87 + frontend/src/layout/AppMenu.vue | 134 + frontend/src/layout/AppMenuItem.vue | 95 + frontend/src/layout/AppSidebar.vue | 11 + frontend/src/layout/AppTopbar.vue | 110 + frontend/src/layout/composables/layout.js | 84 + frontend/src/main.js | 83 + frontend/src/router/index.js | 85 + frontend/src/stores/profile.js | 30 + frontend/src/utils/time.js | 17 + frontend/src/views/Login.vue | 84 + frontend/src/views/NotFound.vue | 52 + frontend/src/views/Register.vue | 83 + frontend/src/views/admin/Images.vue | 118 + frontend/src/views/admin/Instances.vue | 609 ++ frontend/src/views/admin/Servers.vue | 404 ++ frontend/src/views/admin/Users.vue | 186 + frontend/src/views/users/InstanceCreate.vue | 282 + frontend/src/views/users/InstanceList.vue | 611 ++ frontend/src/views/users/Settings.vue | 3 + frontend/tailwind.config.js | 15 + frontend/vite.config.mjs | 36 + go.mod | 65 + go.sum | 211 + libs/crypto/base64.go | 11 + libs/crypto/hex.go | 45 + libs/crypto/sha256.go | 13 + libs/crypto/uuid.go | 24 + libs/logger/logger.go | 225 + libs/logger/logger_test.go | 48 + libs/request/request.go | 281 + main.go | 53 + models/instance.go | 61 + models/orders.go | 27 + models/servers.go | 32 + models/system.go | 6 + models/users.go | 32 + routers/.gitignore | 1 + routers/api/v1/admin/images/list.go | 35 + routers/api/v1/admin/images/modify.go | 48 + routers/api/v1/admin/images/routers.go | 22 + routers/api/v1/admin/instance/add.go | 104 + routers/api/v1/admin/instance/control.go | 135 + routers/api/v1/admin/instance/delete.go | 61 + routers/api/v1/admin/instance/detail.go | 53 + routers/api/v1/admin/instance/label.go | 49 + routers/api/v1/admin/instance/list.go | 88 + routers/api/v1/admin/instance/modify.go | 128 + routers/api/v1/admin/instance/routers.go | 37 + routers/api/v1/admin/routers.go | 20 + routers/api/v1/admin/servers/add.go | 56 + routers/api/v1/admin/servers/delete.go | 43 + routers/api/v1/admin/servers/detail.go | 39 + routers/api/v1/admin/servers/list.go | 62 + routers/api/v1/admin/servers/modify.go | 84 + routers/api/v1/admin/servers/routers.go | 40 + routers/api/v1/admin/users/add.go | 1 + routers/api/v1/admin/users/delete.go | 49 + routers/api/v1/admin/users/detail.go | 31 + routers/api/v1/admin/users/list.go | 53 + routers/api/v1/admin/users/modify.go | 65 + routers/api/v1/admin/users/routers.go | 22 + routers/api/v1/middleware/code.go | 87 + routers/api/v1/middleware/middleware.go | 71 + routers/api/v1/middleware/response.go | 41 + routers/api/v1/middleware/struct.go | 12 + routers/api/v1/routers.go | 13 + routers/api/v1/sessions/session.go | 14 + routers/api/v1/user/images/list.go | 35 + routers/api/v1/user/images/routers.go | 20 + routers/api/v1/user/instances/add.go | 99 + routers/api/v1/user/instances/control.go | 141 + routers/api/v1/user/instances/delete.go | 65 + routers/api/v1/user/instances/detail.go | 61 + routers/api/v1/user/instances/label.go | 55 + routers/api/v1/user/instances/list.go | 85 + routers/api/v1/user/instances/modify.go | 134 + routers/api/v1/user/instances/routers.go | 38 + routers/api/v1/user/login.go | 71 + routers/api/v1/user/logout.go | 26 + routers/api/v1/user/orders.go | 1 + routers/api/v1/user/profile.go | 46 + routers/api/v1/user/register.go | 52 + routers/api/v1/user/resetPassword.go | 55 + routers/api/v1/user/routers.go | 32 + routers/api/v1/user/servers/detail.go | 39 + routers/api/v1/user/servers/list.go | 62 + routers/api/v1/user/servers/routers.go | 19 + routers/index/routers.go | 13 + routers/index/web.go | 14 + routers/index/web/.gitkeep | 1 + routers/middleware.go | 11 + routers/routers.go | 21 + services/config/config.go | 74 + services/config/exporter.go | 58 + services/config/internal.go | 65 + services/database/database.go | 39 + services/database/migrate.go | 34 + services/dispatcher/add.go | 57 + services/dispatcher/control.go | 128 + services/dispatcher/delete.go | 58 + services/dispatcher/init.go | 45 + services/dispatcher/interface.go | 33 + services/dispatcher/modify.go | 84 + services/dispatcher/push.go | 33 + services/dispatcher/run.go | 91 + services/http/http.go | 62 + services/instanceController/chatTable.go | 32 + services/instanceController/continue.go | 74 + services/instanceController/create.go | 69 + services/instanceController/createInstance.go | 93 + services/instanceController/createVolume.go | 66 + services/instanceController/delete.go | 47 + services/instanceController/deleteInstance.go | 39 + services/instanceController/deleteVolume.go | 45 + services/instanceController/execute.go | 58 + services/instanceController/getPortForward.go | 59 + services/instanceController/history.go | 1 + services/instanceController/init.go | 25 + services/instanceController/patch.go | 162 + services/instanceController/patchCpuOnly.go | 73 + services/instanceController/patchGpu.go | 81 + services/instanceController/patchVolume.go | 64 + services/instanceController/pause.go | 74 + services/instanceController/restart.go | 99 + services/instanceController/rollback.go | 1 + services/instanceController/stop.go | 74 + services/logger/logger.go | 13 + services/redis/redis.go | 61 + services/system/check.go | 25 + services/system/init.go | 36 + 172 files changed, 16348 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 config.example.yml create mode 100644 docker-compose.example.yml create mode 100644 frontend/.editorconfig create mode 100644 frontend/.eslintrc.cjs create mode 100644 frontend/.gitignore create mode 100644 frontend/.prettierrc.json create mode 100644 frontend/index.html create mode 100644 frontend/jsconfig.json create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api.js create mode 100644 frontend/src/assets/layout/_core.scss create mode 100644 frontend/src/assets/layout/_footer.scss create mode 100644 frontend/src/assets/layout/_main.scss create mode 100644 frontend/src/assets/layout/_menu.scss create mode 100644 frontend/src/assets/layout/_mixins.scss create mode 100644 frontend/src/assets/layout/_preloading.scss create mode 100644 frontend/src/assets/layout/_responsive.scss create mode 100644 frontend/src/assets/layout/_topbar.scss create mode 100644 frontend/src/assets/layout/_typography.scss create mode 100644 frontend/src/assets/layout/_utils.scss create mode 100644 frontend/src/assets/layout/layout.scss create mode 100644 frontend/src/assets/layout/variables/_common.scss create mode 100644 frontend/src/assets/layout/variables/_dark.scss create mode 100644 frontend/src/assets/layout/variables/_light.scss create mode 100644 frontend/src/assets/logo-text.webp create mode 100755 frontend/src/assets/logo.svg create mode 100644 frontend/src/assets/styles.scss create mode 100644 frontend/src/assets/tailwind.css create mode 100644 frontend/src/components/CopyIcon.vue create mode 100644 frontend/src/components/EditLabel.vue create mode 100644 frontend/src/components/FloatingConfigurator.vue create mode 100644 frontend/src/components/SectionBanner.vue create mode 100644 frontend/src/layout/AppFooter.vue create mode 100644 frontend/src/layout/AppLayout.vue create mode 100644 frontend/src/layout/AppMenu.vue create mode 100644 frontend/src/layout/AppMenuItem.vue create mode 100644 frontend/src/layout/AppSidebar.vue create mode 100644 frontend/src/layout/AppTopbar.vue create mode 100644 frontend/src/layout/composables/layout.js create mode 100644 frontend/src/main.js create mode 100644 frontend/src/router/index.js create mode 100644 frontend/src/stores/profile.js create mode 100644 frontend/src/utils/time.js create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/src/views/NotFound.vue create mode 100644 frontend/src/views/Register.vue create mode 100644 frontend/src/views/admin/Images.vue create mode 100644 frontend/src/views/admin/Instances.vue create mode 100644 frontend/src/views/admin/Servers.vue create mode 100644 frontend/src/views/admin/Users.vue create mode 100644 frontend/src/views/users/InstanceCreate.vue create mode 100644 frontend/src/views/users/InstanceList.vue create mode 100644 frontend/src/views/users/Settings.vue create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/vite.config.mjs create mode 100644 go.mod create mode 100644 go.sum create mode 100644 libs/crypto/base64.go create mode 100644 libs/crypto/hex.go create mode 100644 libs/crypto/sha256.go create mode 100644 libs/crypto/uuid.go create mode 100644 libs/logger/logger.go create mode 100644 libs/logger/logger_test.go create mode 100644 libs/request/request.go create mode 100644 main.go create mode 100644 models/instance.go create mode 100644 models/orders.go create mode 100644 models/servers.go create mode 100644 models/system.go create mode 100644 models/users.go create mode 100644 routers/.gitignore create mode 100644 routers/api/v1/admin/images/list.go create mode 100644 routers/api/v1/admin/images/modify.go create mode 100644 routers/api/v1/admin/images/routers.go create mode 100644 routers/api/v1/admin/instance/add.go create mode 100644 routers/api/v1/admin/instance/control.go create mode 100644 routers/api/v1/admin/instance/delete.go create mode 100644 routers/api/v1/admin/instance/detail.go create mode 100644 routers/api/v1/admin/instance/label.go create mode 100644 routers/api/v1/admin/instance/list.go create mode 100644 routers/api/v1/admin/instance/modify.go create mode 100644 routers/api/v1/admin/instance/routers.go create mode 100644 routers/api/v1/admin/routers.go create mode 100644 routers/api/v1/admin/servers/add.go create mode 100644 routers/api/v1/admin/servers/delete.go create mode 100644 routers/api/v1/admin/servers/detail.go create mode 100644 routers/api/v1/admin/servers/list.go create mode 100644 routers/api/v1/admin/servers/modify.go create mode 100644 routers/api/v1/admin/servers/routers.go create mode 100644 routers/api/v1/admin/users/add.go create mode 100644 routers/api/v1/admin/users/delete.go create mode 100644 routers/api/v1/admin/users/detail.go create mode 100644 routers/api/v1/admin/users/list.go create mode 100644 routers/api/v1/admin/users/modify.go create mode 100644 routers/api/v1/admin/users/routers.go create mode 100644 routers/api/v1/middleware/code.go create mode 100644 routers/api/v1/middleware/middleware.go create mode 100644 routers/api/v1/middleware/response.go create mode 100644 routers/api/v1/middleware/struct.go create mode 100644 routers/api/v1/routers.go create mode 100644 routers/api/v1/sessions/session.go create mode 100644 routers/api/v1/user/images/list.go create mode 100644 routers/api/v1/user/images/routers.go create mode 100644 routers/api/v1/user/instances/add.go create mode 100644 routers/api/v1/user/instances/control.go create mode 100644 routers/api/v1/user/instances/delete.go create mode 100644 routers/api/v1/user/instances/detail.go create mode 100644 routers/api/v1/user/instances/label.go create mode 100644 routers/api/v1/user/instances/list.go create mode 100644 routers/api/v1/user/instances/modify.go create mode 100644 routers/api/v1/user/instances/routers.go create mode 100644 routers/api/v1/user/login.go create mode 100644 routers/api/v1/user/logout.go create mode 100644 routers/api/v1/user/orders.go create mode 100644 routers/api/v1/user/profile.go create mode 100644 routers/api/v1/user/register.go create mode 100644 routers/api/v1/user/resetPassword.go create mode 100644 routers/api/v1/user/routers.go create mode 100644 routers/api/v1/user/servers/detail.go create mode 100644 routers/api/v1/user/servers/list.go create mode 100644 routers/api/v1/user/servers/routers.go create mode 100644 routers/index/routers.go create mode 100644 routers/index/web.go create mode 100644 routers/index/web/.gitkeep create mode 100644 routers/middleware.go create mode 100644 routers/routers.go create mode 100644 services/config/config.go create mode 100644 services/config/exporter.go create mode 100644 services/config/internal.go create mode 100644 services/database/database.go create mode 100644 services/database/migrate.go create mode 100644 services/dispatcher/add.go create mode 100644 services/dispatcher/control.go create mode 100644 services/dispatcher/delete.go create mode 100644 services/dispatcher/init.go create mode 100644 services/dispatcher/interface.go create mode 100644 services/dispatcher/modify.go create mode 100644 services/dispatcher/push.go create mode 100644 services/dispatcher/run.go create mode 100644 services/http/http.go create mode 100644 services/instanceController/chatTable.go create mode 100644 services/instanceController/continue.go create mode 100644 services/instanceController/create.go create mode 100644 services/instanceController/createInstance.go create mode 100644 services/instanceController/createVolume.go create mode 100644 services/instanceController/delete.go create mode 100644 services/instanceController/deleteInstance.go create mode 100644 services/instanceController/deleteVolume.go create mode 100644 services/instanceController/execute.go create mode 100644 services/instanceController/getPortForward.go create mode 100644 services/instanceController/history.go create mode 100644 services/instanceController/init.go create mode 100644 services/instanceController/patch.go create mode 100644 services/instanceController/patchCpuOnly.go create mode 100644 services/instanceController/patchGpu.go create mode 100644 services/instanceController/patchVolume.go create mode 100644 services/instanceController/pause.go create mode 100644 services/instanceController/restart.go create mode 100644 services/instanceController/rollback.go create mode 100644 services/instanceController/stop.go create mode 100644 services/logger/logger.go create mode 100644 services/redis/redis.go create mode 100644 services/system/check.go create mode 100644 services/system/init.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..950075e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +data + +config.yml +docker-compose.yml + +megrez \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c0a6e5a --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..53786f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript]": { + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.formatOnSave": true + }, + "[vue]": { + "editor.defaultFormatter": "Vue.volar", + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bae94e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/config.example.yml b/config.example.yml new file mode 100644 index 0000000..9a4fb78 --- /dev/null +++ b/config.example.yml @@ -0,0 +1,12 @@ +http: + host: 0.0.0.0 + port: 34567 +database: + host: gm-postgres + port: 5432 + username: GpuManager + password: GpuManager + database: GpuManager +redis: + host: ms-redis + port: 6379 diff --git a/docker-compose.example.yml b/docker-compose.example.yml new file mode 100644 index 0000000..68cd832 --- /dev/null +++ b/docker-compose.example.yml @@ -0,0 +1,41 @@ +version: "3" + +services: + megrez: + image: zklcdc/ubuntu-with-tzdata:latest + container_name: megrez + restart: always + environment: + - TZ=Asia/Shanghai + volumes: + - $PWD/megrez:/home/megrez + - $PWD/config.yaml:/home/config.yaml + depends_on: + - "megrez-redis" + - "megrez-postgres" + command: sh -c "cd /home/ && /home/megrez -config /home/config.yml" + ports: + - 34567:34567 + + megrez-postgres: + image: postgres:16-alpine + container_name: megrez-postgres + restart: always + volumes: + - $PWD/data/postgres:/var/lib/postgresql/data + environment: + - TZ=Asia/Shanghai + - POSTGRES_DB=GpuManager + - POSTGRES_USER=GpuManager + - POSTGRES_PASSWORD=GpuManager + + megrez-redis: + image: redis:7-alpine + container_name: megrez-redis + restart: always + volumes: + - $PWD/data/redis:/data + +networks: + default: + name: megrez-network \ No newline at end of file diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 0000000..b585a76 --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 0000000..ec96cff --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,23 @@ +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution'); + +module.exports = { + root: true, + env: { + node: true + }, + extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-prettier'], + parserOptions: { + ecmaVersion: 'latest' + }, + rules: { + 'vue/multi-word-component-names': 'off', + 'vue/no-reserved-component-names': 'off', + 'vue/component-tags-order': [ + 'error', + { + order: ['script', 'template', 'style'] + } + ] + } +}; diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..d45df1c --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,14 @@ +node_modules +coverage +*.log* +.nuxt +.nitro +.cache +.output +.env +dist +.DS_Store +.idea +.eslintcache +api-generator/typedoc.json +**/.DS_Store diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000..6e1c17b --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "useTabs": false, + "tabWidth": 2, + "trailingComma": "none", + "semi": true, + "singleQuote": true, + "vueIndentScriptAndStyle": false, + "printWidth": 250, + "bracketSameLine": false +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..7d82c64 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + MEGREZ 天权算能聚联计算平台 + + + +
+ + + + \ No newline at end of file diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 0000000..aafb595 --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..2ca5ab0 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5113 @@ +{ + "name": "megrez-ui", + "version": "4.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "megrez-ui", + "version": "4.1.0", + "dependencies": { + "@microsoft/clarity": "^1.0.0", + "@primevue/themes": "^4.0.0", + "axios": "^1.7.7", + "chart.js": "3.3.2", + "pinia": "^2.2.4", + "pinia-plugin-persistedstate": "^4.1.1", + "primeicons": "^6.0.1", + "primevue": "^4.0.0", + "vue": "^3.4.34", + "vue-router": "^4.4.0" + }, + "devDependencies": { + "@primevue/auto-import-resolver": "^4.0.1", + "@rushstack/eslint-patch": "^1.8.0", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/eslint-config-prettier": "^9.0.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.23.0", + "postcss": "^8.4.40", + "prettier": "^3.2.5", + "sass": "^1.55.0", + "tailwindcss": "^3.4.6", + "tailwindcss-primeui": "^0.3.2", + "unplugin-vue-components": "^0.27.3", + "vite": "^5.3.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.25.7.tgz", + "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.25.7.tgz", + "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.25.7.tgz", + "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/standalone": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/standalone/-/standalone-7.25.7.tgz", + "integrity": "sha512-7H+mK18Ew4C/pIIiZwF1eiVjUEh2Ju/BpwRZwcPeXltF/rIjHjFL0gol7PtGrHocmIq6P6ubJrylmmWQ3lGJPA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.25.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.25.7.tgz", + "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@microsoft/clarity": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@microsoft/clarity/-/clarity-1.0.0.tgz", + "integrity": "sha512-2QY6SmXnqRj6dWhNY8NYCN3e53j4zCFebH4wGnNhdGV1mqAsQwql2fT0w8TISxCvwwfVp8idsWLIdrRHOms1PQ==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxt/kit": { + "version": "3.13.2", + "resolved": "https://registry.npmmirror.com/@nuxt/kit/-/kit-3.13.2.tgz", + "integrity": "sha512-KvRw21zU//wdz25IeE1E5m/aFSzhJloBRAQtv+evcFeZvuroIxpIQuUqhbzuwznaUwpiWbmwlcsp5uOWmi4vwA==", + "license": "MIT", + "dependencies": { + "@nuxt/schema": "3.13.2", + "c12": "^1.11.2", + "consola": "^3.2.3", + "defu": "^6.1.4", + "destr": "^2.0.3", + "globby": "^14.0.2", + "hash-sum": "^2.0.0", + "ignore": "^5.3.2", + "jiti": "^1.21.6", + "klona": "^2.0.6", + "knitwork": "^1.1.0", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "semver": "^7.6.3", + "ufo": "^1.5.4", + "unctx": "^2.3.1", + "unimport": "^3.12.0", + "untyped": "^1.4.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/schema": { + "version": "3.13.2", + "resolved": "https://registry.npmmirror.com/@nuxt/schema/-/schema-3.13.2.tgz", + "integrity": "sha512-CCZgpm+MkqtOMDEgF9SWgGPBXlQ01hV/6+2reDEpJuqFPGzV8HYKPBcIFvn7/z5ahtgutHLzjP71Na+hYcqSpw==", + "license": "MIT", + "dependencies": { + "compatx": "^0.1.8", + "consola": "^3.2.3", + "defu": "^6.1.4", + "hookable": "^5.5.3", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "std-env": "^3.7.0", + "ufo": "^1.5.4", + "uncrypto": "^0.1.3", + "unimport": "^3.12.0", + "untyped": "^1.4.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@primeuix/styled": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.0.5.tgz", + "integrity": "sha512-pVoGn/uPkVm/DyF3TR3EmH/pL/dP4nR42FcYbVduFq9VfO3KVeOEqvcCULHXos66RZO9MCbCFUoLy6ctf9GUGQ==", + "dependencies": { + "@primeuix/utils": "^0.0.5" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primeuix/utils": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.0.5.tgz", + "integrity": "sha512-ntUiUgtRtkF8KuaxHffzhYxQxoXk6LAPHm7CVlFjdqS8Rx8xRkLkZVyo84E+pO2hcNFkOGVP/GxHhQ2s94O8zA==", + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/auto-import-resolver": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@primevue/auto-import-resolver/-/auto-import-resolver-4.0.3.tgz", + "integrity": "sha512-zwWEeIwgIJcDbW1lz6YIn8du6BY9Q3mSnBRZUYLNWVw3fqlO78vKbOLD3brY8PNVvZMRxT7zWH0c+WtGK8bUtA==", + "dev": true, + "dependencies": { + "@primevue/metadata": "4.0.3" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/core": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.0.3.tgz", + "integrity": "sha512-pKdXsYHRMtJfw1tjPJ2u3bC+QLUDIftdHgR7DRfZ4ZV0/9qOGpFZIBYTv3Px+NGQnV+qAyp3Yeh/pS9UO83wHQ==", + "dependencies": { + "@primeuix/styled": "^0.0.5", + "@primeuix/utils": "^0.0.5" + }, + "engines": { + "node": ">=12.11.0" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@primevue/icons": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.0.3.tgz", + "integrity": "sha512-eamWkgXhAt/3nC0HJLYmkpXf6DP9QDQ/ecaqT9onNlHXHXL+QDd25OG/wKHPKB94jh3AO/9rD1aMW+YtXtkKbQ==", + "dependencies": { + "@primeuix/utils": "^0.0.5", + "@primevue/core": "4.0.3" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/metadata": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@primevue/metadata/-/metadata-4.0.3.tgz", + "integrity": "sha512-xL3BlD9Nx5gCCQAHyK2BMujUm34FmBlODuFyevWzEX/lkhzWs7jLVQ5lUpoaiXm+jcdgzq7K0QDA8OxCVITucA==", + "dev": true, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/themes": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.0.3.tgz", + "integrity": "sha512-5Mcb8UES2wg9WtvuK/lKW61xZpkbmRGdxKBh3LfEsxx8pwRmFtRD8XCXUPzaxUv9lwVEW3F/AQ0oAyBbnXptoQ==", + "dependencies": { + "@primeuix/styled": "^0.0.5" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz", + "integrity": "sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz", + "integrity": "sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz", + "integrity": "sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz", + "integrity": "sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz", + "integrity": "sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz", + "integrity": "sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz", + "integrity": "sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz", + "integrity": "sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz", + "integrity": "sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz", + "integrity": "sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz", + "integrity": "sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz", + "integrity": "sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz", + "integrity": "sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz", + "integrity": "sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz", + "integrity": "sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz", + "integrity": "sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", + "dev": true + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.1.tgz", + "integrity": "sha512-sDckXxlHpMsjRQbAH9WanangrfrblsOd3pNifePs+FOHjJg1jfWq5L/P0PsBRndEt3nmdUnmvieP8ULDeX5AvA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.34.tgz", + "integrity": "sha512-Z0izUf32+wAnQewjHu+pQf1yw00EGOmevl1kE+ljjjMe7oEfpQ+BI3/JNK7yMB4IrUsqLDmPecUrpj3mCP+yJQ==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.34", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.34.tgz", + "integrity": "sha512-3PUOTS1h5cskdOJMExCu2TInXuM0j60DRPpSCJDqOCupCfUZCJoyQmKtRmA8EgDNZ5kcEE7vketamRZfrEuVDw==", + "dependencies": { + "@vue/compiler-core": "3.4.34", + "@vue/shared": "3.4.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.34.tgz", + "integrity": "sha512-x6lm0UrM03jjDXTPZgD9Ad8bIVD1ifWNit2EaWQIZB5CULr46+FbLQ5RpK7AXtDHGjx9rmvC7QRCTjsiGkAwRw==", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.34", + "@vue/compiler-dom": "3.4.34", + "@vue/compiler-ssr": "3.4.34", + "@vue/shared": "3.4.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.10", + "postcss": "^8.4.39", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.34.tgz", + "integrity": "sha512-8TDBcLaTrFm5rnF+Qm4BlliaopJgqJ28Nsrc80qazynm5aJO+Emu7y0RWw34L8dNnTRdcVBpWzJxhGYzsoVu4g==", + "dependencies": { + "@vue/compiler-dom": "3.4.34", + "@vue/shared": "3.4.34" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", + "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==" + }, + "node_modules/@vue/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==", + "dev": true, + "dependencies": { + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0" + }, + "peerDependencies": { + "eslint": ">= 8.0.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.34.tgz", + "integrity": "sha512-ua+Lo+wBRlBEX9TtgPOShE2JwIO7p6BTZ7t1KZVPoaBRfqbC7N3c8Mpzicx173fXxx5VXeU6ykiHo7WgLzJQDA==", + "dependencies": { + "@vue/shared": "3.4.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.34.tgz", + "integrity": "sha512-PXhkiRPwcPGJ1BnyBZFI96GfInCVskd0HPNIAZn7i3YOmLbtbTZpB7/kDTwC1W7IqdGPkTVC63IS7J2nZs4Ebg==", + "dependencies": { + "@vue/reactivity": "3.4.34", + "@vue/shared": "3.4.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.34.tgz", + "integrity": "sha512-dXqIe+RqFAK2Euak4UsvbIupalrhc67OuQKpD7HJ3W2fv8jlqvI7szfBCsAEcE8o/wyNpkloxB6J8viuF/E3gw==", + "dependencies": { + "@vue/reactivity": "3.4.34", + "@vue/runtime-core": "3.4.34", + "@vue/shared": "3.4.34", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.34.tgz", + "integrity": "sha512-GeyEUfMVRZMD/mZcNONEqg7MiU10QQ1DB3O/Qr6+8uXpbwdlmVgQ5Qs1/ZUAFX1X2UUtqMoGrDRbxdWfOJFT7Q==", + "dependencies": { + "@vue/compiler-ssr": "3.4.34", + "@vue/shared": "3.4.34" + }, + "peerDependencies": { + "vue": "3.4.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.34.tgz", + "integrity": "sha512-x5LmiRLpRsd9KTjAB8MPKf0CDPMcuItjP0gbNqFCIgL1I8iYp4zglhj9w9FPCdIbHG2M91RVeIbArFfFTz9I3A==" + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/c12": { + "version": "1.11.2", + "resolved": "https://registry.npmmirror.com/c12/-/c12-1.11.2.tgz", + "integrity": "sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "confbox": "^0.1.7", + "defu": "^6.1.4", + "dotenv": "^16.4.5", + "giget": "^1.2.3", + "jiti": "^1.21.6", + "mlly": "^1.7.1", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.4" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001667", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", + "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chart.js": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.3.2.tgz", + "integrity": "sha512-H0hSO7xqTIrwxoACqnSoNromEMfXvfuVnrbuSt2TuXfBDDofbnto4zuZlRtRvC73/b37q3wGAWZyUU41QPvNbA==" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmmirror.com/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compatx": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/compatx/-/compatx-0.1.8.tgz", + "integrity": "sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deep-pick-omit": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/deep-pick-omit/-/deep-pick-omit-1.2.1.tgz", + "integrity": "sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==", + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.34", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.34.tgz", + "integrity": "sha512-/TZAiChbAflBNjCg+VvstbcwAtIL/VdMFO3NgRFIzBjpvPzWOTIbbO8kNb6RwU4bt9TP7K+3KqBKw/lOU+Y+GA==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz", + "integrity": "sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.0", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/giget": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "tar": "^6.2.0" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmmirror.com/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "license": "MIT" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "9.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.0.tgz", + "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/knitwork": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/knitwork/-/knitwork-1.1.0.tgz", + "integrity": "sha512-oHnmiBUVHz1V+URE77PNot2lv3QiYU2zQf1JjOVkMt3YDKGbu8NAFr+c4mcNOhdsGrB/VpVbRwPwhiXrPhxQbw==", + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mlly": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.7.2.tgz", + "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.12.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nypm": { + "version": "0.3.12", + "resolved": "https://registry.npmmirror.com/nypm/-/nypm-0.3.12.tgz", + "integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "execa": "^8.0.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ohash": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/ohash/-/ohash-1.1.4.tgz", + "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "2.2.4", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.2.4.tgz", + "integrity": "sha512-K7ZhpMY9iJ9ShTC0cR2+PnxdQRuwVIsXDO/WIEV/RnMC/vmSoKDTKW/exNQYPI+4ij10UjXqdNiEHwn47McANQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia-plugin-persistedstate": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.1.1.tgz", + "integrity": "sha512-fUiUsbfBetGUZzX28z+ImAZw7FDXzwRrk+fN+ljF5OhQMhsSYfYeUzI9FLLtpjekYbfFHWvJiECkLI60RIuiPA==", + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.13.2", + "deep-pick-omit": "^1.2.1", + "defu": "^6.1.4", + "destr": "^2.0.3" + }, + "peerDependencies": { + "@pinia/nuxt": ">=0.5.0", + "pinia": ">=2.0.0" + }, + "peerDependenciesMeta": { + "@pinia/nuxt": { + "optional": true + }, + "pinia": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.2.1.tgz", + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.2", + "pathe": "^1.1.2" + } + }, + "node_modules/postcss": { + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", + "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/primeicons": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/primeicons/-/primeicons-6.0.1.tgz", + "integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==", + "license": "MIT" + }, + "node_modules/primevue": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.0.3.tgz", + "integrity": "sha512-f/41JMb7pMP++zKuiymXYmDMk/4oFRlJKXQzgrAy5Q3esU29hS59MBaP2CsHdPoJasQSzVAgOT+36S4/WXLajg==", + "dependencies": { + "@primeuix/styled": "^0.0.5", + "@primeuix/utils": "^0.0.5", + "@primevue/core": "4.0.3", + "@primevue/icons": "4.0.3" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.1.tgz", + "integrity": "sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==", + "devOptional": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.19.1", + "@rollup/rollup-android-arm64": "4.19.1", + "@rollup/rollup-darwin-arm64": "4.19.1", + "@rollup/rollup-darwin-x64": "4.19.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.19.1", + "@rollup/rollup-linux-arm-musleabihf": "4.19.1", + "@rollup/rollup-linux-arm64-gnu": "4.19.1", + "@rollup/rollup-linux-arm64-musl": "4.19.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.19.1", + "@rollup/rollup-linux-riscv64-gnu": "4.19.1", + "@rollup/rollup-linux-s390x-gnu": "4.19.1", + "@rollup/rollup-linux-x64-gnu": "4.19.1", + "@rollup/rollup-linux-x64-musl": "4.19.1", + "@rollup/rollup-win32-arm64-msvc": "4.19.1", + "@rollup/rollup-win32-ia32-msvc": "4.19.1", + "@rollup/rollup-win32-x64-msvc": "4.19.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sass": { + "version": "1.77.8", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", + "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.0.tgz", + "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz", + "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-primeui": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/tailwindcss-primeui/-/tailwindcss-primeui-0.3.4.tgz", + "integrity": "sha512-5+Qfoe5Kpq2Iwrd6umBUb3rQH6b7+pL4jxJUId0Su5agUM6TwCyH5Pyl9R0y3QQB3IRuTxBNmeS11B41f+30zw==", + "dev": true, + "peerDependencies": { + "tailwindcss": ">=3.1.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmmirror.com/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/unctx": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/unctx/-/unctx-2.3.1.tgz", + "integrity": "sha512-PhKke8ZYauiqh3FEMVNm7ljvzQiph0Mt3GBRve03IJm7ukfaON2OBK795tLwhbyfzknuRRkW0+Ze+CQUmzOZ+A==", + "license": "MIT", + "dependencies": { + "acorn": "^8.8.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.0", + "unplugin": "^1.3.1" + } + }, + "node_modules/unctx/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport": { + "version": "3.13.1", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.13.1.tgz", + "integrity": "sha512-nNrVzcs93yrZQOW77qnyOVHtb68LegvhYFwxFMfuuWScmwQmyVCG/NBuN8tYsaGzgQUVYv34E/af+Cc9u4og4A==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.2", + "acorn": "^8.12.1", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.0", + "unplugin": "^1.14.1" + } + }, + "node_modules/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unplugin": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.14.1.tgz", + "integrity": "sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w==", + "license": "MIT", + "dependencies": { + "acorn": "^8.12.1", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "webpack-sources": "^3" + }, + "peerDependenciesMeta": { + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.27.3.tgz", + "integrity": "sha512-5wg7lbdg5ZcrAQNzyYK+6gcg/DG8K6rO+f5YeuvqGHs/PhpapBvpA4O/0ex/pFthE5WgRk43iWuRZEMLVsdz4Q==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.0", + "chokidar": "^3.6.0", + "debug": "^4.3.5", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.10", + "minimatch": "^9.0.5", + "mlly": "^1.7.1", + "unplugin": "^1.11.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/unplugin-vue-components/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/untyped": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/untyped/-/untyped-1.5.1.tgz", + "integrity": "sha512-reBOnkJBFfBZ8pCKaeHgfZLcehXtM6UTxc+vqs1JvCps0c4amLNp3fhdGBZwYp+VLyoY9n3X5KOP7lCyWBUX9A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.7", + "@babel/standalone": "^7.25.7", + "@babel/types": "^7.25.7", + "defu": "^6.1.4", + "jiti": "^2.3.1", + "mri": "^1.2.0", + "scule": "^1.3.0" + }, + "bin": { + "untyped": "dist/cli.mjs" + } + }, + "node_modules/untyped/node_modules/jiti": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.3.3.tgz", + "integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.34.tgz", + "integrity": "sha512-VZze05HWlA3ItreQ/ka7Sx7PoD0/3St8FEiSlSTVgb6l4hL+RjtP2/8g5WQBzZgyf8WG2f+g1bXzC7zggLhAJA==", + "dependencies": { + "@vue/compiler-dom": "3.4.34", + "@vue/compiler-sfc": "3.4.34", + "@vue/runtime-dom": "3.4.34", + "@vue/server-renderer": "3.4.34", + "@vue/shared": "3.4.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz", + "integrity": "sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==", + "dependencies": { + "@vue/devtools-api": "^6.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "optional": true, + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..d9b1d43 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "megrez-ui", + "version": "4.1.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint --fix . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" + }, + "dependencies": { + "@microsoft/clarity": "^1.0.0", + "@primevue/themes": "^4.0.0", + "axios": "^1.7.7", + "chart.js": "3.3.2", + "pinia": "^2.2.4", + "pinia-plugin-persistedstate": "^4.1.1", + "primeicons": "^6.0.1", + "primevue": "^4.0.0", + "vue": "^3.4.34", + "vue-router": "^4.4.0" + }, + "devDependencies": { + "@primevue/auto-import-resolver": "^4.0.1", + "@rushstack/eslint-patch": "^1.8.0", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/eslint-config-prettier": "^9.0.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.23.0", + "postcss": "^8.4.40", + "prettier": "^3.2.5", + "sass": "^1.55.0", + "tailwindcss": "^3.4.6", + "tailwindcss-primeui": "^0.3.2", + "unplugin-vue-components": "^0.27.3", + "vite": "^5.3.1" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..fe66dd6 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..19915881fcf51cbdeba3baede5b16266fbfe597a GIT binary patch literal 9440 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 UqZty$C|q${L+@7kP3y4#0bP4O&j0`b literal 0 HcmV?d00001 diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..d81a9e5 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,8 @@ + + + + + diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..a9a4d41 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,126 @@ +import axios from "axios"; + +const ajax = (url, method, { params = {}, data = {} }) => { + axios.defaults.withCredentials = true + axios.defaults.crossDomain = true + axios.defaults.baseURL = '/api/v1' + return new Promise((resolve, reject) => { + axios({ + url, + method, + params, + data + }).then(res => { + if (res.data.code != 200) { + reject(res) + } else { + resolve(res) + } + }, res => { + reject(res) + }) + }) +} + +export default { + GetUserProfile() { + return ajax('user/profile', 'get', {}) + }, + UserLogin(data) { + return ajax('user/login', 'post', { data }) + }, + UserLogout() { + return ajax('user/logout', 'get', {}) + }, + UserRegister(data) { + return ajax('user/register', 'post', { data }) + }, + + UserInstancesList(params) { + return ajax('user/instances', 'get', { params }) + }, + UserInstancesDetail(id) { + return ajax(`user/instances/${id}`, 'get', {}) + }, + UserInstancesModify(id, data) { + return ajax(`user/instances/${id}`, 'post', { data }) + }, + UserInstancesModifyLabel(id, data) { + return ajax(`user/instances/${id}/label`, 'post', { data }) + }, + UserInstancesAction(id, data) { + return ajax(`user/instances/${id}/`, 'put', { data }) + }, + UserInstancesCreate(data) { + return ajax('user/instances', 'post', { data }) + }, + UserInstancesDelete(id) { + return ajax(`user/instances/${id}`, 'delete', {}) + }, + + UserServerList(params) { + return ajax('user/servers', 'get', { params }) + }, + UserServerDetail(id) { + return ajax(`user/servers/${id}`, 'get', {}) + }, + + UserImages() { + return ajax('user/images', 'get', {}) + }, + + AdminInstancesList(params) { + return ajax('admin/instances', 'get', { params }) + }, + AdminInstancesDetail(id) { + return ajax(`admin/instances/${id}`, 'get', {}) + }, + AdminInstancesModify(id, data) { + return ajax(`admin/instances/${id}`, 'post', { data }) + }, + AdminInstancesModifyLabel(id, data) { + return ajax(`admin/instances/${id}/label`, 'post', { data }) + }, + AdminInstancesAction(id, data) { + return ajax(`admin/instances/${id}/`, 'put', { data }) + }, + AdminInstancesCreate(data) { + return ajax('admin/instances', 'post', { data }) + }, + AdminInstancesDelete(id) { + return ajax(`admin/instances/${id}`, 'delete', {}) + }, + + AdminUserList(params) { + return ajax('admin/users', 'get', { params }) + }, + AdminUserModify(id, data) { + return ajax(`admin/users/${id}`, 'post', { data }) + }, + AdminUserDelete(id) { + return ajax(`admin/users/${id}`, 'delete', {}) + }, + + AdminServersList(params) { + return ajax('admin/servers', 'get', { params }) + }, + AdminServersDetail(id) { + return ajax(`admin/servers/${id}`, 'get', {}) + }, + AdminServersAdd(data) { + return ajax('admin/servers', 'post', { data }) + }, + AdminServersModify(id, data) { + return ajax(`admin/servers/${id}`, 'post', { data }) + }, + AdminServersDelete(id) { + return ajax(`admin/servers/${id}`, 'delete', {}) + }, + + AdminImagesList() { + return ajax('admin/images', 'get', {}) + }, + AdminImagesModify(data) { + return ajax('admin/images', 'post', { data }) + }, +} \ No newline at end of file diff --git a/frontend/src/assets/layout/_core.scss b/frontend/src/assets/layout/_core.scss new file mode 100644 index 0000000..920f351 --- /dev/null +++ b/frontend/src/assets/layout/_core.scss @@ -0,0 +1,23 @@ +html { + height: 100%; + font-size: 14px; +} + +body { + font-family: 'Lato', sans-serif; + color: var(--text-color); + // background-color: var(--surface-ground); + margin: 0; + padding: 0; + min-height: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + text-decoration: none; +} + +.layout-wrapper { + min-height: 100vh; +} diff --git a/frontend/src/assets/layout/_footer.scss b/frontend/src/assets/layout/_footer.scss new file mode 100644 index 0000000..27bcbf0 --- /dev/null +++ b/frontend/src/assets/layout/_footer.scss @@ -0,0 +1,8 @@ +.layout-footer { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem 0 1rem 0; + gap: 0.5rem; + border-top: 1px solid var(--surface-border); +} diff --git a/frontend/src/assets/layout/_main.scss b/frontend/src/assets/layout/_main.scss new file mode 100644 index 0000000..943dbc7 --- /dev/null +++ b/frontend/src/assets/layout/_main.scss @@ -0,0 +1,16 @@ +.layout-main-container { + display: flex; + flex-direction: column; + min-height: 100vh; + justify-content: space-between; + padding: 2rem 2rem 0 2rem; + margin-top: 4rem; + transition: margin-left var(--layout-section-transition-duration); + border-radius: 1rem; + background-color: var(--surface-ground); +} + +.layout-main { + flex: 1 1 auto; + padding-bottom: 2rem; +} diff --git a/frontend/src/assets/layout/_menu.scss b/frontend/src/assets/layout/_menu.scss new file mode 100644 index 0000000..05a6141 --- /dev/null +++ b/frontend/src/assets/layout/_menu.scss @@ -0,0 +1,160 @@ +.layout-sidebar { + position: fixed; + width: 20rem; + // height: calc(100vh - 8rem); + height: calc(100vh - 4rem); + z-index: 999; + overflow-y: auto; + user-select: none; + // top: 6rem; + top: 4rem; + // left: 2rem; + transition: + transform var(--layout-section-transition-duration), + left var(--layout-section-transition-duration); + background-color: var(--surface-overlay); + // border-radius: var(--content-border-radius); + padding: 0.5rem 1.5rem; +} + +.layout-menu { + margin: 0; + padding: 0; + list-style-type: none; + + .layout-root-menuitem { + > .layout-menuitem-root-text { + font-size: 0.857rem; + text-transform: uppercase; + font-weight: 700; + color: var(--text-color); + margin: 0.75rem 0; + } + + > a { + display: none; + } + } + + a { + user-select: none; + + &.active-menuitem { + > .layout-submenu-toggler { + transform: rotate(-180deg); + } + } + } + + li.active-menuitem { + > a { + .layout-submenu-toggler { + transform: rotate(-180deg); + } + } + } + + ul { + margin: 0; + padding: 0; + list-style-type: none; + + a { + display: flex; + align-items: center; + position: relative; + outline: 0 none; + color: var(--text-color); + cursor: pointer; + padding: 0.75rem 1rem; + border-radius: var(--content-border-radius); + transition: + background-color var(--element-transition-duration), + box-shadow var(--element-transition-duration); + + .layout-menuitem-icon { + margin-right: 0.5rem; + } + + .layout-submenu-toggler { + font-size: 75%; + margin-left: auto; + transition: transform var(--element-transition-duration); + } + + &.active-route { + font-weight: 700; + color: var(--primary-color); + } + + &:hover { + background-color: var(--surface-hover); + } + + &:focus { + @include focused-inset(); + } + } + + ul { + overflow: hidden; + border-radius: var(--content-border-radius); + + li { + a { + margin-left: 1rem; + } + + li { + a { + margin-left: 2rem; + } + + li { + a { + margin-left: 2.5rem; + } + + li { + a { + margin-left: 3rem; + } + + li { + a { + margin-left: 3.5rem; + } + + li { + a { + margin-left: 4rem; + } + } + } + } + } + } + } + } + } +} + +.layout-submenu-enter-from, +.layout-submenu-leave-to { + max-height: 0; +} + +.layout-submenu-enter-to, +.layout-submenu-leave-from { + max-height: 1000px; +} + +.layout-submenu-leave-active { + overflow: hidden; + transition: max-height 0.45s cubic-bezier(0, 1, 0, 1); +} + +.layout-submenu-enter-active { + overflow: hidden; + transition: max-height 1s ease-in-out; +} diff --git a/frontend/src/assets/layout/_mixins.scss b/frontend/src/assets/layout/_mixins.scss new file mode 100644 index 0000000..ad330b1 --- /dev/null +++ b/frontend/src/assets/layout/_mixins.scss @@ -0,0 +1,15 @@ +@mixin focused() { + outline-width: var(--focus-ring-width); + outline-style: var(--focus-ring-style); + outline-color: var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); + box-shadow: var(--focus-ring-shadow); + transition: + box-shadow var(--transition-duration), + outline-color var(--transition-duration); +} + +@mixin focused-inset() { + outline-offset: -1px; + box-shadow: inset var(--focus-ring-shadow); +} diff --git a/frontend/src/assets/layout/_preloading.scss b/frontend/src/assets/layout/_preloading.scss new file mode 100644 index 0000000..a814104 --- /dev/null +++ b/frontend/src/assets/layout/_preloading.scss @@ -0,0 +1,47 @@ +.preloader { + position: fixed; + z-index: 999999; + background: #edf1f5; + width: 100%; + height: 100%; +} +.preloader-content { + border: 0 solid transparent; + border-radius: 50%; + width: 150px; + height: 150px; + position: absolute; + top: calc(50vh - 75px); + left: calc(50vw - 75px); +} + +.preloader-content:before, .preloader-content:after{ + content: ''; + border: 1em solid var(--primary-color); + border-radius: 50%; + width: inherit; + height: inherit; + position: absolute; + top: 0; + left: 0; + animation: loader 2s linear infinite; + opacity: 0; +} + +.preloader-content:before{ + animation-delay: 0.5s; +} + +@keyframes loader{ + 0%{ + transform: scale(0); + opacity: 0; + } + 50%{ + opacity: 1; + } + 100%{ + transform: scale(1); + opacity: 0; + } +} diff --git a/frontend/src/assets/layout/_responsive.scss b/frontend/src/assets/layout/_responsive.scss new file mode 100644 index 0000000..9f5d418 --- /dev/null +++ b/frontend/src/assets/layout/_responsive.scss @@ -0,0 +1,110 @@ +@media screen and (min-width: 1960px) { + .layout-main, + .landing-wrapper { + width: 1504px; + margin-left: auto !important; + margin-right: auto !important; + } +} + +@media (min-width: 992px) { + .layout-wrapper { + &.layout-overlay { + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + + .layout-sidebar { + transform: translateX(-100%); + left: 0; + top: 0; + height: 100vh; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-right: 1px solid var(--surface-border); + transition: + transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99), + left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99); + box-shadow: + 0px 3px 5px rgba(0, 0, 0, 0.02), + 0px 0px 2px rgba(0, 0, 0, 0.05), + 0px 1px 4px rgba(0, 0, 0, 0.08); + } + + &.layout-overlay-active { + .layout-sidebar { + transform: translateX(0); + } + } + } + + &.layout-static { + .layout-main-container { + margin-left: 20rem; + } + + &.layout-static-inactive { + .layout-sidebar { + transform: translateX(-100%); + left: 0; + } + + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + } + } + + .layout-mask { + display: none; + } + } +} + +@media (max-width: 991px) { + .blocked-scroll { + overflow: hidden; + } + + .layout-wrapper { + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + + .layout-sidebar { + transform: translateX(-100%); + left: 0; + top: 0; + height: 100vh; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + transition: + transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99), + left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99); + } + + .layout-mask { + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 998; + width: 100%; + height: 100%; + background-color: var(--maskbg); + } + + &.layout-mobile-active { + .layout-sidebar { + transform: translateX(0); + } + + .layout-mask { + display: block; + } + } + } +} diff --git a/frontend/src/assets/layout/_topbar.scss b/frontend/src/assets/layout/_topbar.scss new file mode 100644 index 0000000..3338f25 --- /dev/null +++ b/frontend/src/assets/layout/_topbar.scss @@ -0,0 +1,204 @@ +.layout-topbar { + position: fixed; + height: 4rem; + z-index: 997; + left: 0; + top: 0; + width: 100%; + padding: 0 2rem; + background-color: var(--surface-card); + transition: left var(--layout-section-transition-duration); + display: flex; + align-items: center; + + .layout-topbar-logo-container { + width: 28rem; + display: flex; + align-items: center; + } + + .layout-topbar-logo { + display: inline-flex; + align-items: center; + font-size: 1.5rem; + border-radius: var(--content-border-radius); + color: var(--text-color); + font-weight: 500; + gap: 0.8rem; + + svg { + width: 3rem; + } + + img { + width: 2rem; + } + + &:focus-visible { + @include focused(); + } + } + + .layout-topbar-action { + display: inline-flex; + justify-content: center; + align-items: center; + color: var(--text-color-secondary); + border-radius: 50%; + width: 2.5rem; + height: 2.5rem; + color: var(--text-color); + transition: background-color var(--element-transition-duration); + cursor: pointer; + + &:hover { + background-color: var(--surface-hover); + } + + &:focus-visible { + @include focused(); + } + + i { + font-size: 1.25rem; + } + + span { + font-size: 1rem; + display: none; + } + + &.layout-topbar-action-highlight { + background-color: var(--primary-color); + color: var(--primary-contrast-color); + } + } + + .layout-menu-button { + margin-right: 0.5rem; + } + + .layout-topbar-menu-button { + display: none; + } + + .layout-topbar-actions { + margin-left: auto; + display: flex; + gap: 1rem; + } + + .layout-topbar-menu-content { + display: flex; + gap: 1rem; + } + + .layout-config-menu { + display: flex; + gap: 1rem; + } +} + +@media (max-width: 991px) { + .layout-topbar { + padding: 0 2rem; + + .layout-topbar-logo-container { + width: auto; + } + + .layout-menu-button { + margin-left: 0; + margin-right: 0.5rem; + } + + .layout-topbar-menu-button { + display: inline-flex; + } + + .layout-topbar-menu { + position: absolute; + background-color: var(--surface-overlay); + transform-origin: top; + box-shadow: + 0px 3px 5px rgba(0, 0, 0, 0.02), + 0px 0px 2px rgba(0, 0, 0, 0.05), + 0px 1px 4px rgba(0, 0, 0, 0.08); + border-radius: var(--content-border-radius); + padding: 1rem; + right: 2rem; + top: 4rem; + min-width: 15rem; + border: 1px solid var(--surface-border); + + .layout-topbar-menu-content { + gap: 0.5rem; + } + + .layout-topbar-action { + display: flex; + width: 100%; + height: auto; + justify-content: flex-start; + border-radius: var(--content-border-radius); + padding: 0.5rem 1rem; + + i { + font-size: 1rem; + margin-right: 0.5rem; + } + + span { + font-weight: medium; + display: block; + } + } + } + + .layout-topbar-menu-content { + flex-direction: column; + } + } +} + +.config-panel { + .config-panel-label { + font-size: 0.875rem; + color: var(--text-secondary-color); + font-weight: 600; + line-height: 1; + } + + .config-panel-colors { + > div { + padding-top: 0.5rem; + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + justify-content: space-between; + + button { + border: none; + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; + padding: 0; + cursor: pointer; + outline-color: transparent; + outline-width: 2px; + outline-style: solid; + outline-offset: 1px; + + &.active-color { + outline-color: var(--primary-color); + } + } + } + } + + .config-panel-settings { + display: flex; + flex-direction: column; + gap: 0.5rem; + } +} diff --git a/frontend/src/assets/layout/_typography.scss b/frontend/src/assets/layout/_typography.scss new file mode 100644 index 0000000..b17bbc2 --- /dev/null +++ b/frontend/src/assets/layout/_typography.scss @@ -0,0 +1,68 @@ +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1.5rem 0 1rem 0; + font-family: inherit; + font-weight: 700; + line-height: 1.5; + color: var(--text-color); + + &:first-child { + margin-top: 0; + } +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.75rem; +} + +h4 { + font-size: 1.5rem; +} + +h5 { + font-size: 1.25rem; +} + +h6 { + font-size: 1rem; +} + +mark { + background: #fff8e1; + padding: 0.25rem 0.4rem; + border-radius: var(--content-border-radius); + font-family: monospace; +} + +blockquote { + margin: 1rem 0; + padding: 0 2rem; + border-left: 4px solid #90a4ae; +} + +hr { + border-top: solid var(--surface-border); + border-width: 1px 0 0 0; + margin: 1rem 0; +} + +p { + margin: 0 0 1rem 0; + line-height: 1.5; + + &:last-child { + margin-bottom: 0; + } +} diff --git a/frontend/src/assets/layout/_utils.scss b/frontend/src/assets/layout/_utils.scss new file mode 100644 index 0000000..6ccec88 --- /dev/null +++ b/frontend/src/assets/layout/_utils.scss @@ -0,0 +1,25 @@ +/* Utils */ +.clearfix:after { + content: ' '; + display: block; + clear: both; +} + +.card { + background: var(--surface-card); + padding: 2rem; + margin-bottom: 2rem; + border-radius: var(--content-border-radius); + + &:last-child { + margin-bottom: 0; + } +} + +.p-toast { + &.p-toast-top-right, + &.p-toast-top-left, + &.p-toast-top-center { + top: 100px; + } +} diff --git a/frontend/src/assets/layout/layout.scss b/frontend/src/assets/layout/layout.scss new file mode 100644 index 0000000..8971f62 --- /dev/null +++ b/frontend/src/assets/layout/layout.scss @@ -0,0 +1,13 @@ +@import './variables/_common'; +@import './variables/_light'; +@import './variables/_dark'; +@import './_mixins'; +@import './_preloading'; +@import './_core'; +@import './_main'; +@import './_topbar'; +@import './_menu'; +@import './_footer'; +@import './_responsive'; +@import './_utils'; +@import './_typography'; diff --git a/frontend/src/assets/layout/variables/_common.scss b/frontend/src/assets/layout/variables/_common.scss new file mode 100644 index 0000000..2a040c2 --- /dev/null +++ b/frontend/src/assets/layout/variables/_common.scss @@ -0,0 +1,20 @@ +:root { + --primary-color: var(--p-primary-color); + --primary-contrast-color: var(--p-primary-contrast-color); + --text-color: var(--p-text-color); + --text-color-secondary: var(--p-text-muted-color); + --surface-border: var(--p-content-border-color); + --surface-card: var(--p-content-background); + --surface-hover: var(--p-content-hover-background); + --surface-overlay: var(--p-overlay-popover-background); + --transition-duration: var(--p-transition-duration); + --maskbg: var(--p-mask-background); + --content-border-radius: var(--p-content-border-radius); + --layout-section-transition-duration: 0.2s; + --element-transition-duration: var(--p-transition-duration); + --focus-ring-width: var(--p-focus-ring-width); + --focus-ring-style: var(--p-focus-ring-style); + --focus-ring-color: var(--p-focus-ring-color); + --focus-ring-offset: var(--p-focus-ring-offset); + --focus-ring-shadow: var(--p-focus-ring-shadow); +} diff --git a/frontend/src/assets/layout/variables/_dark.scss b/frontend/src/assets/layout/variables/_dark.scss new file mode 100644 index 0000000..bb91605 --- /dev/null +++ b/frontend/src/assets/layout/variables/_dark.scss @@ -0,0 +1,5 @@ +:root[class*='app-dark'] { + --surface-ground: var(--p-surface-950); + --code-background: var(--p-surface-800); + --code-color: var(--p-surface-100); +} diff --git a/frontend/src/assets/layout/variables/_light.scss b/frontend/src/assets/layout/variables/_light.scss new file mode 100644 index 0000000..aa3403c --- /dev/null +++ b/frontend/src/assets/layout/variables/_light.scss @@ -0,0 +1,5 @@ +:root { + --surface-ground: var(--p-surface-100); + --code-background: var(--p-surface-900); + --code-color: var(--p-surface-200); +} diff --git a/frontend/src/assets/logo-text.webp b/frontend/src/assets/logo-text.webp new file mode 100644 index 0000000000000000000000000000000000000000..7909385cb6d1e15d1a60d0e5927b82deb9564236 GIT binary patch literal 56630 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 + + Layer 1 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/styles.scss b/frontend/src/assets/styles.scss new file mode 100644 index 0000000..5a22be2 --- /dev/null +++ b/frontend/src/assets/styles.scss @@ -0,0 +1,4 @@ +/* You can add global styles to this file, and also import other style files */ +@import '@/assets/layout/layout.scss'; +@import 'primeicons/primeicons.css'; + diff --git a/frontend/src/assets/tailwind.css b/frontend/src/assets/tailwind.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/frontend/src/assets/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/frontend/src/components/CopyIcon.vue b/frontend/src/components/CopyIcon.vue new file mode 100644 index 0000000..6b49189 --- /dev/null +++ b/frontend/src/components/CopyIcon.vue @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/EditLabel.vue b/frontend/src/components/EditLabel.vue new file mode 100644 index 0000000..29a12e7 --- /dev/null +++ b/frontend/src/components/EditLabel.vue @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/FloatingConfigurator.vue b/frontend/src/components/FloatingConfigurator.vue new file mode 100644 index 0000000..bf8dd58 --- /dev/null +++ b/frontend/src/components/FloatingConfigurator.vue @@ -0,0 +1,12 @@ + + + diff --git a/frontend/src/components/SectionBanner.vue b/frontend/src/components/SectionBanner.vue new file mode 100644 index 0000000..3c6d574 --- /dev/null +++ b/frontend/src/components/SectionBanner.vue @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/frontend/src/layout/AppFooter.vue b/frontend/src/layout/AppFooter.vue new file mode 100644 index 0000000..6917419 --- /dev/null +++ b/frontend/src/layout/AppFooter.vue @@ -0,0 +1,10 @@ + + + diff --git a/frontend/src/layout/AppLayout.vue b/frontend/src/layout/AppLayout.vue new file mode 100644 index 0000000..52046a2 --- /dev/null +++ b/frontend/src/layout/AppLayout.vue @@ -0,0 +1,87 @@ + + + diff --git a/frontend/src/layout/AppMenu.vue b/frontend/src/layout/AppMenu.vue new file mode 100644 index 0000000..ddb9f7f --- /dev/null +++ b/frontend/src/layout/AppMenu.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/frontend/src/layout/AppMenuItem.vue b/frontend/src/layout/AppMenuItem.vue new file mode 100644 index 0000000..9cff3cd --- /dev/null +++ b/frontend/src/layout/AppMenuItem.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/frontend/src/layout/AppSidebar.vue b/frontend/src/layout/AppSidebar.vue new file mode 100644 index 0000000..c6ea0db --- /dev/null +++ b/frontend/src/layout/AppSidebar.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/frontend/src/layout/AppTopbar.vue b/frontend/src/layout/AppTopbar.vue new file mode 100644 index 0000000..62b0c50 --- /dev/null +++ b/frontend/src/layout/AppTopbar.vue @@ -0,0 +1,110 @@ + + + + \ No newline at end of file diff --git a/frontend/src/layout/composables/layout.js b/frontend/src/layout/composables/layout.js new file mode 100644 index 0000000..d8a4cef --- /dev/null +++ b/frontend/src/layout/composables/layout.js @@ -0,0 +1,84 @@ +import { computed, reactive, readonly } from 'vue'; + +const layoutConfig = reactive({ + preset: 'Aura', + primary: 'blue', + surface: null, + darkTheme: false, + menuMode: 'static' +}); + +const layoutState = reactive({ + staticMenuDesktopInactive: false, + overlayMenuActive: false, + profileSidebarVisible: false, + configSidebarVisible: false, + staticMenuMobileActive: false, + menuHoverActive: false, + activeMenuItem: null +}); + +export function useLayout() { + const setPrimary = (value) => { + layoutConfig.primary = value; + }; + + const setSurface = (value) => { + layoutConfig.surface = value; + }; + + const setPreset = (value) => { + layoutConfig.preset = value; + }; + + const setActiveMenuItem = (item) => { + layoutState.activeMenuItem = item.value || item; + }; + + const setMenuMode = (mode) => { + layoutConfig.menuMode = mode; + }; + + const toggleDarkMode = () => { + if (!document.startViewTransition) { + executeDarkModeToggle(); + + return; + } + + document.startViewTransition(() => executeDarkModeToggle(event)); + }; + + const executeDarkModeToggle = () => { + layoutConfig.darkTheme = !layoutConfig.darkTheme; + document.documentElement.classList.toggle('app-dark'); + }; + + const onMenuToggle = () => { + if (layoutConfig.menuMode === 'overlay') { + layoutState.overlayMenuActive = !layoutState.overlayMenuActive; + } + + if (window.innerWidth > 991) { + layoutState.staticMenuDesktopInactive = !layoutState.staticMenuDesktopInactive; + } else { + layoutState.staticMenuMobileActive = !layoutState.staticMenuMobileActive; + } + }; + + const resetMenu = () => { + layoutState.overlayMenuActive = false; + layoutState.staticMenuMobileActive = false; + layoutState.menuHoverActive = false; + }; + + const isSidebarActive = computed(() => layoutState.overlayMenuActive || layoutState.staticMenuMobileActive); + + const isDarkTheme = computed(() => layoutConfig.darkTheme); + + const getPrimary = computed(() => layoutConfig.primary); + + const getSurface = computed(() => layoutConfig.surface); + + return { layoutConfig: readonly(layoutConfig), layoutState: readonly(layoutState), onMenuToggle, isSidebarActive, isDarkTheme, getPrimary, getSurface, setActiveMenuItem, toggleDarkMode, setPrimary, setSurface, setPreset, resetMenu, setMenuMode }; +} diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..e7a7a61 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,83 @@ +import { createApp } from 'vue'; +import App from './App.vue'; +import router from './router'; + +import { createPinia } from 'pinia'; +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; + +import { updatePreset } from '@primevue/themes'; +import Aura from '@primevue/themes/aura'; +import PrimeVue from 'primevue/config'; +import ConfirmationService from 'primevue/confirmationservice'; +import ToastService from 'primevue/toastservice'; + +import '@/assets/styles.scss'; +import '@/assets/tailwind.css'; + +const app = createApp(App); + +const pinia = createPinia() +pinia.use(piniaPluginPersistedstate) + +app.use(pinia); +app.use(router); +app.use(PrimeVue, { + theme: { + preset: Aura, + options: { + darkModeSelector: '.app-dark' + } + }, + pt: { + card: { + root: { + class: 'rounded-2xl' + } + }, + message: { + root: { + class: 'rounded-2xl' + } + } + } +}); +app.use(ToastService); +app.use(ConfirmationService); + +updatePreset({ + semantic: { + primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' }, + colorScheme: { + light: { + primary: { + color: '{primary.500}', + contrastColor: '#ffffff', + hoverColor: '{primary.600}', + activeColor: '{primary.700}' + }, + highlight: { + background: '{primary.50}', + focusBackground: '{primary.100}', + color: '{primary.700}', + focusColor: '{primary.800}' + } + }, + dark: { + primary: { + color: '{primary.400}', + contrastColor: '{surface.900}', + hoverColor: '{primary.300}', + activeColor: '{primary.200}' + }, + highlight: { + background: 'color-mix(in srgb, {primary.400}, transparent 84%)', + focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)', + color: 'rgba(255,255,255,.87)', + focusColor: 'rgba(255,255,255,.87)' + } + } + } + } +}); + +app.mount('#app'); diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..7e7b625 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,85 @@ +import AppLayout from '@/layout/AppLayout.vue'; +import { createRouter, createWebHistory } from 'vue-router'; + +import Login from '@/views/Login.vue'; +import Register from '@/views/Register.vue'; + +import InstanceCreate from '@/views/users/InstanceCreate.vue'; +import InstanceList from '@/views/users/InstanceList.vue'; +import Settings from '@/views/users/Settings.vue'; + +import Images from '@/views/admin/Images.vue'; +import Instances from '@/views/admin/Instances.vue'; +import Servers from '@/views/admin/Servers.vue'; +import Users from '@/views/admin/Users.vue'; + +import NotFound from '@/views/NotFound.vue'; + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + redirect: '/instances' + }, + { + path: '/login', + name: 'login', + component: Login + }, + { + path: '/register', + name: 'register', + component: Register + }, + { + path: '/', + name: 'dashboard', + component: AppLayout, + children: [ + { + path: 'instances', + name: 'instances', + component: InstanceList + }, + { + path: 'instances/create', + name: 'instances-create', + component: InstanceCreate + }, + { + path: 'settings', + name: 'settings', + component: Settings + }, + { + path: 'admin/images', + name: 'admin-images', + component: Images + }, + { + path: 'admin/instances', + name: 'admin-instances', + component: Instances + }, + { + path: 'admin/servers', + name: 'admin-servers', + component: Servers + }, + { + path: 'admin/users', + name: 'admin-users', + component: Users + } + ] + }, + { + path: '/:pathMatch(.*)*', + name: 'not-found', + component: NotFound + } + ] +}); + +export default router; diff --git a/frontend/src/stores/profile.js b/frontend/src/stores/profile.js new file mode 100644 index 0000000..de6d3f3 --- /dev/null +++ b/frontend/src/stores/profile.js @@ -0,0 +1,30 @@ +import { defineStore } from 'pinia'; + +export const useProfileStore = defineStore('profile', { + state: () => ({ + userProfile: { + username: '', + email: '', + role: 0 + } + }), + actions: { + setUserProfile(profile) { + this.userProfile = profile; + }, + clearUserProfile() { + this.userProfile = { + username: '', + email: '', + role: 0 + }; + } + }, + getters: { + isAuthenticated: (state) => !!state.userProfile.email, + isAdmin: (state) => state.userProfile.role == 2, + isSuperAdmin: (state) => state.userProfile.role === 3, + username: (state) => state.userProfile.username, + }, + persist: true // 开启持久化 +}); \ No newline at end of file diff --git a/frontend/src/utils/time.js b/frontend/src/utils/time.js new file mode 100644 index 0000000..3124aca --- /dev/null +++ b/frontend/src/utils/time.js @@ -0,0 +1,17 @@ +function formatDate(dateString) { + const date = new Date(dateString); + + // 自定义格式化输出 (YYYY-MM-DD HH:mm:ss) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从 0 开始 + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` +} + +export { + formatDate +}; diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..3d24506 --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/frontend/src/views/NotFound.vue b/frontend/src/views/NotFound.vue new file mode 100644 index 0000000..4b89a4c --- /dev/null +++ b/frontend/src/views/NotFound.vue @@ -0,0 +1,52 @@ + + + diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue new file mode 100644 index 0000000..51bfad9 --- /dev/null +++ b/frontend/src/views/Register.vue @@ -0,0 +1,83 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Images.vue b/frontend/src/views/admin/Images.vue new file mode 100644 index 0000000..c6cedf0 --- /dev/null +++ b/frontend/src/views/admin/Images.vue @@ -0,0 +1,118 @@ + + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Instances.vue b/frontend/src/views/admin/Instances.vue new file mode 100644 index 0000000..30065c6 --- /dev/null +++ b/frontend/src/views/admin/Instances.vue @@ -0,0 +1,609 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Servers.vue b/frontend/src/views/admin/Servers.vue new file mode 100644 index 0000000..56db1fa --- /dev/null +++ b/frontend/src/views/admin/Servers.vue @@ -0,0 +1,404 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/admin/Users.vue b/frontend/src/views/admin/Users.vue new file mode 100644 index 0000000..a575c23 --- /dev/null +++ b/frontend/src/views/admin/Users.vue @@ -0,0 +1,186 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/users/InstanceCreate.vue b/frontend/src/views/users/InstanceCreate.vue new file mode 100644 index 0000000..4478dab --- /dev/null +++ b/frontend/src/views/users/InstanceCreate.vue @@ -0,0 +1,282 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/users/InstanceList.vue b/frontend/src/views/users/InstanceList.vue new file mode 100644 index 0000000..9487764 --- /dev/null +++ b/frontend/src/views/users/InstanceList.vue @@ -0,0 +1,611 @@ + + + \ No newline at end of file diff --git a/frontend/src/views/users/Settings.vue b/frontend/src/views/users/Settings.vue new file mode 100644 index 0000000..2ac9230 --- /dev/null +++ b/frontend/src/views/users/Settings.vue @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..3519d0b --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,15 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ['selector', '[class*="app-dark"]'], + content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], + plugins: [require('tailwindcss-primeui')], + theme: { + screens: { + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', + '2xl': '1920px' + } + } +}; diff --git a/frontend/vite.config.mjs b/frontend/vite.config.mjs new file mode 100644 index 0000000..7f34388 --- /dev/null +++ b/frontend/vite.config.mjs @@ -0,0 +1,36 @@ +import { fileURLToPath, URL } from 'node:url'; + +import { PrimeVueResolver } from '@primevue/auto-import-resolver'; +import vue from '@vitejs/plugin-vue'; +import Components from 'unplugin-vue-components/vite'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + build: { + outDir: '../routers/index/web', + }, + optimizeDeps: { + noDiscovery: true + }, + plugins: [ + vue(), + Components({ + resolvers: [PrimeVueResolver()] + }) + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + proxy: { + '/api': { + target: 'http://127.0.0.1:34567/api', + // changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '') + } + } + } +}); diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..211c87c --- /dev/null +++ b/go.mod @@ -0,0 +1,65 @@ +module megrez + +go 1.23.1 + +require ( + github.com/google/uuid v1.6.0 + github.com/kataras/iris/v12 v12.2.11 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/postgres v1.5.9 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect + github.com/CloudyKit/jet/v6 v6.2.0 // indirect + github.com/Joker/jade v1.1.3 // indirect + github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/flosch/pongo2/v4 v4.0.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/iris-contrib/schema v0.0.6 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/kataras/blocks v0.0.8 // indirect + github.com/kataras/golog v0.1.11 // indirect + github.com/kataras/pio v0.0.13 // indirect + github.com/kataras/sitemap v0.0.6 // indirect + github.com/kataras/tunnel v0.0.4 // indirect + github.com/klauspost/compress v1.17.7 // indirect + github.com/mailgun/raymond/v2 v2.0.48 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect + github.com/redis/go-redis/v9 v9.5.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/schollz/closestmatch v2.1.0+incompatible // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/tdewolff/minify/v2 v2.20.19 // indirect + github.com/tdewolff/parse/v2 v2.7.12 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/yosssi/ace v0.0.5 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..60f71b4 --- /dev/null +++ b/go.sum @@ -0,0 +1,211 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= +github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= +github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= +github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= +github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 h1:4gjrh/PN2MuWCCElk8/I4OCKRKWCCo2zEct3VKCbibU= +github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go= +github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= +github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= +github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM= +github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg= +github.com/kataras/golog v0.1.11 h1:dGkcCVsIpqiAMWTlebn/ZULHxFvfG4K43LF1cNWSh20= +github.com/kataras/golog v0.1.11/go.mod h1:mAkt1vbPowFUuUGvexyQ5NFW6djEgGyxQBIARJ0AH4A= +github.com/kataras/iris/v12 v12.2.11 h1:sGgo43rMPfzDft8rjVhPs6L3qDJy3TbBrMD/zGL1pzk= +github.com/kataras/iris/v12 v12.2.11/go.mod h1:uMAeX8OqG9vqdhyrIPv8Lajo/wXTtAF43wchP9WHt2w= +github.com/kataras/pio v0.0.13 h1:x0rXVX0fviDTXOOLOmr4MUxOabu1InVSTu5itF8CXCM= +github.com/kataras/pio v0.0.13/go.mod h1:k3HNuSw+eJ8Pm2lA4lRhg3DiCjVgHlP8hmXApSej3oM= +github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY= +github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= +github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= +github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= +github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= +github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= +github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= +github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo= +github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM= +github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ= +github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= +github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= +github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= +github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= +gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= +moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= diff --git a/libs/crypto/base64.go b/libs/crypto/base64.go new file mode 100644 index 0000000..5efb658 --- /dev/null +++ b/libs/crypto/base64.go @@ -0,0 +1,11 @@ +package crypto + +import "encoding/base64" + +func Base64Encode(src []byte) string { + return base64.StdEncoding.EncodeToString(src) +} + +func Base64Decode(src string) ([]byte, error) { + return base64.StdEncoding.DecodeString(src) +} diff --git a/libs/crypto/hex.go b/libs/crypto/hex.go new file mode 100644 index 0000000..90fcfd3 --- /dev/null +++ b/libs/crypto/hex.go @@ -0,0 +1,45 @@ +package crypto + +import ( + "math/rand" + "strings" + "time" +) + +const letters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +var src = rand.NewSource(time.Now().UnixNano()) + +const ( + // 6 bits to represent a letter index + letterIdBits = 6 + // All 1-bits as many as letterIdBits + letterIdMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdMax + } + if idx := int(cache & letterIdMask); idx < len(letters) { + sb.WriteByte(letters[idx]) + i-- + } + cache >>= letterIdBits + remain-- + } + return sb.String() +} + +func HexLowercase(n int) string { + return strings.ToLower(Hex(n)) +} + +func HexUpper(n int) string { + return strings.ToUpper(Hex(n)) +} diff --git a/libs/crypto/sha256.go b/libs/crypto/sha256.go new file mode 100644 index 0000000..26c33bd --- /dev/null +++ b/libs/crypto/sha256.go @@ -0,0 +1,13 @@ +package crypto + +import ( + "crypto/sha256" + "encoding/hex" +) + +func Sha256(str string) string { + hash := sha256.New() + hash.Write([]byte(str)) + bytes := hash.Sum(nil) + return hex.EncodeToString(bytes) +} diff --git a/libs/crypto/uuid.go b/libs/crypto/uuid.go new file mode 100644 index 0000000..c9abe26 --- /dev/null +++ b/libs/crypto/uuid.go @@ -0,0 +1,24 @@ +package crypto + +import ( + "strings" + + "github.com/google/uuid" +) + +// GenerateUUID 生成一个新的 UUID +func GenerateUUID() (string, error) { + newUUID, err := uuid.NewUUID() + if err != nil { + return "", err + } + return newUUID.String(), nil +} + +func GenerateUUIDWithoutHyphen() (string, error) { + newUUID, err := uuid.NewUUID() + if err != nil { + return "", err + } + return strings.ReplaceAll(newUUID.String(), "-", ""), nil +} diff --git a/libs/logger/logger.go b/libs/logger/logger.go new file mode 100644 index 0000000..5ada9dd --- /dev/null +++ b/libs/logger/logger.go @@ -0,0 +1,225 @@ +package logger + +import ( + "container/list" + "context" + "errors" + "io" + "log" + "os" + "strings" + "sync" + "time" + + "gorm.io/gorm/logger" +) + +const ( + DEBUG = "DEBUG" + INFO = "INFO" + WARN = "WARN" + ERROR = "ERROR" +) + +const ( + Reset = "\033[0m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" + Magenta = "\033[35m" + Cyan = "\033[36m" + White = "\033[37m" + BlueBold = "\033[34;1m" + MagentaBold = "\033[35;1m" + RedBold = "\033[31;1m" + YellowBold = "\033[33;1m" +) + +var levelArry = [4]string{DEBUG, INFO, WARN, ERROR} +var colorArry = [4]string{MagentaBold, BlueBold, YellowBold, RedBold} + +var levelMap = map[string]int{ + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, +} + +type LoggerStruct struct { + level string + // logger *log.Logger + model string + function string +} + +type Interface interface { + LogMode(logger.LogLevel) Interface + Info(context.Context, string, ...interface{}) + Warn(context.Context, string, ...interface{}) + Error(context.Context, string, ...interface{}) + Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) +} + +type logetContent struct { + format string + v []interface{} +} + +type logQueueStruct struct { + *list.List + Mu sync.Mutex + RWLock sync.RWMutex +} + +var logQueue logQueueStruct +var logLogger *log.Logger + +func init() { + logQueue = logQueueStruct{ + List: list.New(), + } +} + +func logOutput() { + if !logQueue.Mu.TryLock() { + return + } + for logQueue.Len() > 0 { + logQueue.RWLock.RLock() + e := logQueue.Front() + logQueue.RWLock.RUnlock() + switch e.Value.(type) { + case logetContent: + content := e.Value.(logetContent) + logLogger.Printf(content.format, content.v...) + default: + } + logQueue.RWLock.Lock() + logQueue.Remove(e) + logQueue.RWLock.Unlock() + } + logQueue.Mu.Unlock() +} + +func NewLogger(level string, args ...any) (*LoggerStruct, error) { + level = strings.ToUpper(level) + if _, ok := levelMap[level]; !ok { + return nil, errors.New("level not found") + } + var err error + filename := "data/logs/backend.log" + if len(args) > 0 { + if args[0] == "stdout" { + logLogger = log.New(io.MultiWriter(os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds) + return &LoggerStruct{ + level: level, + // logger: log.New(io.MultiWriter(os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds), + }, nil + } + filename = args[0].(string) + } + logFile, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, os.FileMode(0775)) + + if os.IsNotExist(err) { + err = os.MkdirAll("data/logs", os.FileMode(0775)) + if err != nil { + return nil, err + } + logFile, _ = os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, os.FileMode(0775)) + } + + logLogger = log.New(io.MultiWriter(logFile, os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds) + return &LoggerStruct{ + level: level, + // logger: log.New(io.MultiWriter(logPipe, os.Stdout), "", log.Ldate|log.Ltime), + // logger: log.New(io.MultiWriter(logFile, os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds), + }, nil +} + +func (l *LoggerStruct) Close() { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + for logQueue.Len() > 0 { + select { + case <-ctx.Done(): + cancel() + return + default: + continue + } + } +} + +func (l *LoggerStruct) Clone() *LoggerStruct { + return &LoggerStruct{ + level: l.level, + // logger: log.New(io.MultiWriter(logFile, os.Stdout), "", log.Ldate|log.Ltime|log.Lmicroseconds), + model: l.model, + function: l.function, + } +} + +func (l *LoggerStruct) SetModel(model string) { + l.model = model +} + +func (l *LoggerStruct) SetFunction(function string) { + l.function = function +} + +func (l *LoggerStruct) SetLevel(level string) { + l.level = strings.ToUpper(level) +} + +func (l *LoggerStruct) Fatal(v ...interface{}) { + l.Error("%v", v...) + l.Close() + os.Exit(1) +} + +func (l *LoggerStruct) Print(level, format string, v ...interface{}) { + if levelMap[level] >= levelMap[l.level] { + logQueue.RWLock.Lock() + if l.model != "" && l.function != "" { + // l.logger.Printf("- "+colorArry[levelMap[level]]+level+Reset+" - "+Blue+"["+l.model+"."+l.function+"]"+Reset+" - "+format, v...) + logQueue.PushBack(logetContent{ + format: "- " + colorArry[levelMap[level]] + level + Reset + " - " + Blue + "[" + l.model + "." + l.function + "]" + Reset + " - " + format, + v: v, + }) + } else if l.model != "" { + // l.logger.Printf("- "+colorArry[levelMap[level]]+level+Reset+" - "+Blue+"["+l.model+"]"+Reset+" - "+format, v...) + logQueue.PushBack(logetContent{ + format: "- " + colorArry[levelMap[level]] + level + Reset + " - " + Blue + "[" + l.model + "]" + Reset + " - " + format, + v: v, + }) + } else { + // l.logger.Printf("- "+colorArry[levelMap[level]]+level+Reset+" - "+format, v...) + logQueue.PushBack(logetContent{ + format: "- " + colorArry[levelMap[level]] + level + Reset + " - " + format, + v: v, + }) + } + logQueue.RWLock.Unlock() + go logOutput() + } +} + +func (l *LoggerStruct) Println(level string, v ...interface{}) { + l.Print(level, "%v", v...) +} + +func (l *LoggerStruct) Debug(format string, v ...interface{}) { + l.Print(DEBUG, format, v...) +} + +func (l *LoggerStruct) Info(format string, v ...interface{}) { + l.Print(INFO, format, v...) +} + +func (l *LoggerStruct) Warn(format string, v ...interface{}) { + l.Print(WARN, format, v...) +} + +func (l *LoggerStruct) Error(format string, v ...interface{}) { + l.Print(ERROR, format, v...) +} diff --git a/libs/logger/logger_test.go b/libs/logger/logger_test.go new file mode 100644 index 0000000..4a99e43 --- /dev/null +++ b/libs/logger/logger_test.go @@ -0,0 +1,48 @@ +package logger_test + +import ( + "testing" + + "megrez/libs/logger" +) + +func TestLogger(t *testing.T) { + t.Log("TestLogger") + l, err := logger.NewLogger(logger.DEBUG, "stdout") + if err != nil { + t.Fatal(err) + } + l.Info("TestInfo") + l.Warn("TestWarn") + l.Error("TestError") + l.Debug("TestDebug") + // l.Fatal("Fatal") + + t.Log(("SetModel")) + l.SetModel("Logger") + l.Info("SetModel TestInfo") + l.Warn("SetModel TestWarn") + l.Error("SetModel TestError") + l.Debug("SetModel TestDebug") + + t.Log(("SetFunction")) + l.SetFunction("TestLogger") + l.Info("SetFunction TestInfo") + l.Warn("SetFunction TestWarn") + l.Error("SetFunction TestError") + l.Debug("SetFunction TestDebug") + + l.Close() +} + +func BenchmarkLogger(b *testing.B) { + b.Log("BenchmarkLogger") + l, err := logger.NewLogger(logger.DEBUG, "stdout") + if err != nil { + b.Fatal(err) + } + for i := 0; i < b.N; i++ { + go l.Info("BenchmarkLogger: %d", i) + } + l.Close() +} diff --git a/libs/request/request.go b/libs/request/request.go new file mode 100644 index 0000000..3eaa702 --- /dev/null +++ b/libs/request/request.go @@ -0,0 +1,281 @@ +package request + +import ( + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +// Client ==> 客户端实例 +type Client struct { + Request *Request + Cookies []*http.Cookie + Result Result +} + +// Request ==> 请求体 +type Request struct { + Url string + Method string + Data io.Reader + ContentType string + Authorization string + UserAgent string + Header map[string]string + Timeout time.Duration + // The proxy type is determined by the URL scheme. "http", + // "https", and "socks5" are supported. If the scheme is empty, + // + // If Proxy is nil or nil *URL, no proxy is used. + ProxyUrl url.URL + Redirect bool + MaxRedirects int +} + +// Result ==> 结果集 +type Result struct { + Header http.Header + Location *url.URL + Body []byte + Status int +} + +// NewRequest ==> 新建请求 +func NewRequest() *Client { + return &Client{ + Request: &Request{ + Method: "GET", + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", + Header: make(map[string]string), + Redirect: false, + MaxRedirects: 10, + }, + Result: Result{}, + } +} + +// Do ==> 执行请求 +func (c *Client) Do() *Client { + //HTTP请求构造 + request, _ := http.NewRequest(c.Request.Method, c.Request.Url, c.Request.Data) + request.Header.Set("Content-Type", c.Request.ContentType) + if c.Request.Authorization != "" { + request.Header.Set("Authorization", c.Request.Authorization) + } + if c.Request.UserAgent != "" { + request.Header.Set("User-Agent", c.Request.UserAgent) + } + if len(c.Cookies) != 0 { + for _, cookie := range c.Cookies { + request.AddCookie(cookie) + } + } + // 支持自定义Header + for k, v := range c.Request.Header { + request.Header.Set(k, v) + } + + var client *http.Client + if c.Request.ProxyUrl == (url.URL{}) { + client = &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if !c.Request.Redirect { + return http.ErrUseLastResponse + } + if len(via) >= c.Request.MaxRedirects { + return http.ErrUseLastResponse + } + return nil + }, + } + } else { + client = &http.Client{ + Transport: &http.Transport{Proxy: http.ProxyURL(&c.Request.ProxyUrl)}, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if !c.Request.Redirect { + return http.ErrUseLastResponse + } + if len(via) >= c.Request.MaxRedirects { + return http.ErrUseLastResponse + } + return nil + }, + } + } + if c.Request.Timeout != 0 { + client.Timeout = c.Request.Timeout + } + res, err := client.Do(request) + if err != nil { + // fmt.Println(err) + return c + } + if len(res.Cookies()) > 1 { + c.Cookies = append(c.Cookies, res.Cookies()...) + } + defer res.Body.Close() + c.Result.Status = res.StatusCode + c.Result.Body, _ = io.ReadAll(res.Body) + c.Result.Header = res.Header + return c +} + +// Get ==> 定义请求方式 +func (c *Client) Get() *Client { + c.Request.Method = "GET" + return c +} + +// Post ==> 定义请求方式 +func (c *Client) Post() *Client { + c.Request.Method = "POST" + return c +} + +// Put ==> 定义请求方式 +func (c *Client) Put() *Client { + c.Request.Method = "PUT" + return c +} + +// Patch ==> 定义请求方式 +func (c *Client) Patch() *Client { + c.Request.Method = "PATCH" + return c +} + +// Delete ==> 定义请求方式 +func (c *Client) Delete() *Client { + c.Request.Method = "DELETE" + return c +} + +// SetUrl ==> 定义请求目标 +func (c *Client) SetUrl(url ...any) *Client { + c.Request.Url = fmt.Sprintf(url[0].(string), url[1:]...) + return c +} + +// SetMethod ==> 定义请求方法 +func (c *Client) SetMethod(method string) *Client { + c.Request.Method = method + return c +} + +// SetContentType ==> 定义内容类型 +func (c *Client) SetContentType(contentType string) *Client { + c.Request.ContentType = contentType + return c +} + +// SetUserAgent ==> 定义用户代理 +func (c *Client) SetUserAgent(userAgent string) *Client { + c.Request.UserAgent = userAgent + return c +} + +// SetBody ==> 定义请求内容 +func (c *Client) SetBody(body io.Reader) *Client { + c.Request.Data = body + return c +} + +// SerHeaders ==> 定义请求头列表 +func (c *Client) SetHeaders(headers map[string]string) *Client { + c.Request.Header = headers + return c +} + +// SetHeader ==> 定义请求头 +func (c *Client) SetHeader(key, value string) *Client { + c.Request.Header[key] = value + return c +} + +// SetAuthorization ==> 定义身份验证 +func (c *Client) SetAuthorization(credentials string) *Client { + c.Request.Authorization = credentials + return c +} + +// SetTimeOut ==> 设置会话超时上限 +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.Request.Timeout = timeout + return c +} + +// SetCookie ==> 设置Cookie +func (c *Client) SetCookie(cookie *http.Cookie) *Client { + c.Cookies = append(c.Cookies, cookie) + return c +} + +// SetCookies ==> 设置Cookies +func (c *Client) SetCookies(cookies string) *Client { + cookielist := strings.Split(cookies, "; ") + for _, cookie := range cookielist { + cookiekv := strings.Split(cookie, "=") + c.SetCookie(&http.Cookie{ + Name: cookiekv[0], + Value: strings.Join(cookiekv[1:], "="), + }) + } + return c +} + +// SetProxy ==> 设置代理 +func (c *Client) SetProxy(proxyUrl url.URL) *Client { + c.Request.ProxyUrl = proxyUrl + return c +} + +// SetRedirect ==> 设置重定向 +func (c *Client) SetRedirect(redirect bool) *Client { + c.Request.Redirect = redirect + return c +} + +// SetMaxRedirects ==> 设置最大重定向次数 +func (c *Client) SetMaxRedirects(maxRedirects int) *Client { + c.Request.MaxRedirects = maxRedirects + return c +} + +// GetStatusCode ==> 获取请求状态码 +func (c *Client) GetStatusCode() int { + return c.Result.Status +} + +// GetBody ==> 获取返回内容 +func (c *Client) GetBody() []byte { + return c.Result.Body +} + +// GetBody ==> 获取返回内容 +func (c *Client) GetBodyString() string { + return string(c.Result.Body) +} + +// GetHeaders ==> 获取返回头字典 +func (c *Client) GetHeaders() http.Header { + return c.Result.Header +} + +// GetHeader ==> 获取返回头 +func (c *Client) GetHeader(key string) string { + return c.Result.Header.Get(key) +} + +// SaveToFile ==> 写出结果到文件 +func (c *Client) SaveToFile(filepath string) (err error) { + // Write the body to file + err = os.WriteFile(filepath, c.GetBody(), 0777) + if err != nil { + return err + } + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..62287d2 --- /dev/null +++ b/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "flag" + "megrez/services/config" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/http" + "megrez/services/instanceController" + "megrez/services/logger" + "megrez/services/redis" + "megrez/services/system" + "runtime" +) + +var ( + configFilePath = "config.yml" + l = logger.Logger.Clone() +) + +func main() { + flag.StringVar(&configFilePath, "c", "config.yml", "config file path") + flag.Parse() + + config.InitConfig(configFilePath) + + defer func() { + redis.Close() + + l.Close() + }() + defer func() { + defer func() { + if err := recover(); err != nil { + l.Error("Panic: %v", err) + buf := make([]byte, 1024) + n := runtime.Stack(buf, false) + l.Error("Stack trace: \n%s", buf[:n]) + } + }() + }() + + logger.InitLogger(config.GetLogLevel(), config.GetLogFile()) + + database.Connect() + redis.Connect() + instanceController.InitInstanceController() + dispatcher.Init() + + go system.Check() + + http.Start() +} diff --git a/models/instance.go b/models/instance.go new file mode 100644 index 0000000..db5cb9c --- /dev/null +++ b/models/instance.go @@ -0,0 +1,61 @@ +package models + +import ( + "slices" + "time" + + "gorm.io/gorm" +) + +type Status int + +const ( + InstanceFail Status = -1 + InstanceRunning Status = 0 + InstancePaused Status = 1 + InstanceStopped Status = 2 + + InstanceReady Status = 3 + InstanceStarting Status = 4 + InstanceStopping Status = 5 + InstancePausing Status = 6 + InstanceRestarting Status = 7 + InstanceModifying Status = 8 + InstanceDeleting Status = 9 +) + +var instanceIngStatus = []Status{InstanceReady, InstanceStarting, InstanceStopping, InstancePausing, InstanceRestarting, InstanceModifying, InstanceDeleting} + +type Instances struct { + ID uint `json:"id" gorm:"primary_key;autoIncrement;index"` + + UserID uint `json:"user_id,omitempty" gorm:"not null"` + ServerID uint `json:"server_id" gorm:"not null"` + + ImageName string `json:"image_name" gorm:"type:varchar(255);not null"` + ContainerName string `json:"container_name,omitempty" gorm:"type:varchar(255);not null"` + CpuOnly bool `json:"cpu_only" gorm:"not null"` + GpuCount int `json:"gpu_count" gorm:"not null"` + VolumeName string `json:"volume_name,omitempty" gorm:"type:varchar(255);not null"` + VolumeSize int `json:"volume_size" gorm:"not null"` + + SshAddress string `json:"ssh_address" gorm:"type:varchar(255);not null"` + SshPasswd string `json:"ssh_passwd" gorm:"type:varchar(255);not null"` + JupyterAddress string `json:"jupyter_address" gorm:"type:varchar(255);not null"` + TensorBoardAddress string `json:"tensor_board_address" gorm:"type:varchar(255);not null"` + GrafanaAddress string `json:"grafana_address" gorm:"type:varchar(255);not null"` + Status Status `json:"status" gorm:"not null"` // -1: fail, 0: running, 1: paused, 2: stopped, 3: readying + + Label string `json:"label" gorm:"type:varchar(255)"` + + CreatedAt time.Time `json:"create_at"` + UpdatedAt time.Time `json:"update_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func InstanceIngStatusCheck(status Status) bool { + if slices.Index(instanceIngStatus, status) != -1 { + return true + } + return false +} diff --git a/models/orders.go b/models/orders.go new file mode 100644 index 0000000..d7c1aa6 --- /dev/null +++ b/models/orders.go @@ -0,0 +1,27 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type Orders struct { + ID uint `json:"id" gorm:"primary_key;autoIncrement;index"` + + UserID uint `json:"user_id" gorm:"not null"` + ServerID uint `json:"server_id" gorm:"not null"` + StartTime time.Time `json:"start_time" gorm:"not null"` + EndTime time.Time `json:"end_time" gorm:"not null"` + + GpuNum int `json:"gpu_num" gorm:"not null"` + DiskUsed float64 `json:"disk_used" gorm:"not null"` + + PricePerHour float64 `json:"price_per_gpu_per_hour" gorm:"not null"` + PriceDiskPerHour float64 `json:"price_disk_per_hour" gorm:"not null"` + + Price float64 `json:"price" gorm:"not null"` + + CreatedAt time.Time `json:"create_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} diff --git a/models/servers.go b/models/servers.go new file mode 100644 index 0000000..341a5f2 --- /dev/null +++ b/models/servers.go @@ -0,0 +1,32 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type Servers struct { + ID uint `json:"id" gorm:"primary_key;autoIncrement;index"` + + Name string `json:"name" gorm:"type:varchar(255);not null"` + IP string `json:"ip,omitempty" gorm:"type:varchar(255);not null"` + Port int `json:"port,omitempty" gorm:"not null"` + Apikey string `json:"apikey,omitempty" gorm:"type:varchar(255);not null"` + GpuType string `json:"gpu_type" gorm:"type:varchar(255);not null"` + GpuNum int `json:"gpu_num" gorm:"not null"` + GpuDriverVersion string `json:"gpu_driver_version" gorm:"type:varchar(255);not null"` + GpuCudaVersion string `json:"gpu_cuda_version" gorm:"type:varchar(255);not null"` + + CpuCountPerGpu int `json:"cpu_count_per_gpu" gorm:"not null"` + MemoryPerGpu int `json:"memory_per_gpu" gorm:"not null"` // Unit `GB` + VolumeTotal int `json:"volume_total" gorm:"not null"` // Unit `GB` + Price float64 `json:"price" gorm:"not null"` // 1 GPU Per Hour + PriceVolume float64 `json:"price_volume" gorm:"not null"` // 1GB Per Hour + + GpuUsed int `json:"gpu_used" gorm:"not null,default:0"` + VolumeUsed int `json:"volume_used" gorm:"not null,default:0"` + + CreatedAt time.Time `json:"create_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} diff --git a/models/system.go b/models/system.go new file mode 100644 index 0000000..a3ac65b --- /dev/null +++ b/models/system.go @@ -0,0 +1,6 @@ +package models + +type System struct { + Key string `json:"key" gorm:"primary_key;unique;index"` + Value string `json:"value"` +} diff --git a/models/users.go b/models/users.go new file mode 100644 index 0000000..ded9dcc --- /dev/null +++ b/models/users.go @@ -0,0 +1,32 @@ +package models + +import ( + "megrez/libs/crypto" + "megrez/services/config" + "time" + + "gorm.io/gorm" +) + +type Users struct { + ID uint `json:"id" gorm:"primary_key;autoIncrement;index"` + + Username string `json:"username" gorm:"type:varchar(255);uniqueIndex;unique;not null"` + Password string `json:"password,omitempty" gorm:"type:varchar(255);not null"` + Role int `json:"role" gorm:"not null,default:0"` + + Email string `json:"email" gorm:"type:varchar(255);uniqueIndex;unique;not null"` + + Balance float64 `json:"balance" gorm:"not null"` + + CreatedAt time.Time `json:"create_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (u *Users) PasswordHash(password string) string { + return crypto.Sha256(password + u.Email + config.GetSystemSalt()) +} + +func (u *Users) CheckPassword(password string) bool { + return u.Password == u.PasswordHash(password) +} diff --git a/routers/.gitignore b/routers/.gitignore new file mode 100644 index 0000000..6f3439a --- /dev/null +++ b/routers/.gitignore @@ -0,0 +1 @@ +web \ No newline at end of file diff --git a/routers/api/v1/admin/images/list.go b/routers/api/v1/admin/images/list.go new file mode 100644 index 0000000..011df4f --- /dev/null +++ b/routers/api/v1/admin/images/list.go @@ -0,0 +1,35 @@ +package images + +import ( + "encoding/json" + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + + res := make(map[string]string) + + system := models.System{ + Key: imagesKey, + } + result := database.DB.FirstOrCreate(&system) + if result.Error != nil { + l.Error("get system error: %v", result.Error) + middleware.Result(ctx, res) + return + } + + err := json.Unmarshal([]byte(system.Value), &res) + if err != nil { + l.Error("unmarshal system error: %v", err) + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + middleware.Result(ctx, res) +} diff --git a/routers/api/v1/admin/images/modify.go b/routers/api/v1/admin/images/modify.go new file mode 100644 index 0000000..d1d0461 --- /dev/null +++ b/routers/api/v1/admin/images/modify.go @@ -0,0 +1,48 @@ +package images + +import ( + "encoding/json" + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func modifyHandler(ctx iris.Context) { + l.SetFunction("modifyHandler") + + req := make(map[string]string) + err := ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + system := models.System{ + Key: imagesKey, + } + result := database.DB.FirstOrCreate(&system) + if result.Error != nil { + l.Error("get system config error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + valueBytes, err := json.Marshal(req) + if err != nil { + l.Error("marshal system error: %v", err) + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + system.Value = string(valueBytes) + result = database.DB.Save(&system) + if result.Error != nil { + l.Error("save system error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/images/routers.go b/routers/api/v1/admin/images/routers.go new file mode 100644 index 0000000..c3c90d7 --- /dev/null +++ b/routers/api/v1/admin/images/routers.go @@ -0,0 +1,22 @@ +package images + +import ( + "megrez/routers/api/v1/middleware" + "megrez/services/logger" + + "github.com/kataras/iris/v12/core/router" + + _logger "megrez/libs/logger" +) + +const imagesKey = "images" + +var l *_logger.LoggerStruct + +func InitImages(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.Admin.Images") + + party.Get("/", listHandler) + party.Post("/", middleware.SuperAdminCheck, modifyHandler) +} diff --git a/routers/api/v1/admin/instance/add.go b/routers/api/v1/admin/instance/add.go new file mode 100644 index 0000000..ff508d4 --- /dev/null +++ b/routers/api/v1/admin/instance/add.go @@ -0,0 +1,104 @@ +package instances + +import ( + "megrez/libs/crypto" + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type addReqStruct struct { + UserID uint `json:"user_id"` + ServerID uint `json:"server_id"` + + ImageName string `json:"image_name"` + GpuCount int `json:"gpu_count"` + VolumeSize int `json:"volume_size"` +} + +func addHandler(ctx iris.Context) { + l.SetFunction("addHandler") + + var req addReqStruct + err := ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if req.UserID == 0 || req.ServerID == 0 || req.ImageName == "" || req.GpuCount <= 0 || req.VolumeSize < 50 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + } + + user := models.Users{ + ID: req.UserID, + } + result := database.DB.First(&user) + if result.Error != nil { + l.Error("query user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminUserQueryError, iris.StatusInternalServerError) + return + } + + server := models.Servers{ + ID: req.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-req.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + remainVolume, err := redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(-req.VolumeSize-30)).Result() + if err != nil { + l.Error("incrby volume size error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 || remainVolume < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(req.GpuCount)) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(req.VolumeSize+30)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + UserID: req.UserID, + ServerID: req.ServerID, + ImageName: req.ImageName, + GpuCount: req.GpuCount, + VolumeSize: req.VolumeSize, + + SshPasswd: crypto.Hex(16), + + Status: models.InstanceReady, + } + result = database.DB.Create(&instance) + if result.Error != nil { + l.Error("create instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInternalCreateError, iris.StatusInternalServerError) + return + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Add, + InstanceID: instance.ID, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/instance/control.go b/routers/api/v1/admin/instance/control.go new file mode 100644 index 0000000..af4b24a --- /dev/null +++ b/routers/api/v1/admin/instance/control.go @@ -0,0 +1,135 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type controlStruct struct { + Action instanceController.Action `json:"action"` // 1: start, 2: pause , 3: stop, 4: restart +} + +func controlHandler(ctx iris.Context) { + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req controlStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.First(&instance) + if result.Error != nil { + l.Error("detail instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + if models.InstanceIngStatusCheck(instance.Status) { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionStop && instance.Status != models.InstanceRunning && instance.Status != models.InstancePaused { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionPause && instance.Status != models.InstanceRunning { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionStart && instance.Status != models.InstanceStopped && instance.Status != models.InstancePaused { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + status := instance.Status + if status == models.InstanceStopped && (req.Action == instanceController.ActionStart || req.Action == instanceController.ActionRestart) { + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-instance.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(instance.GpuCount)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + + switch req.Action { + case instanceController.ActionStart: + result = database.DB.Model(&instance).Update("status", models.InstanceStarting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceStartError, iris.StatusInternalServerError) + return + } + + case instanceController.ActionPause: + result = database.DB.Model(&instance).Update("status", models.InstancePausing) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstancePauseError, iris.StatusInternalServerError) + return + } + + case instanceController.ActionStop: + result = database.DB.Model(&instance).Update("status", models.InstanceStopping) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceStopError, iris.StatusInternalServerError) + return + } + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(instance.GpuCount)) + + case instanceController.ActionRestart: + result = database.DB.Model(&instance).Update("status", models.InstanceRestarting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceRestartError, iris.StatusInternalServerError) + return + } + + default: + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Control, + InstanceID: instance.ID, + Status: status, + Action: req.Action, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/instance/delete.go b/routers/api/v1/admin/instance/delete.go new file mode 100644 index 0000000..bd38912 --- /dev/null +++ b/routers/api/v1/admin/instance/delete.go @@ -0,0 +1,61 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func deleteHandler(ctx iris.Context) { + l.SetFunction("deleteHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.First(&instance) + if result.Error != nil { + l.Error("get instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDeleteError, iris.StatusInternalServerError) + return + } + + status := instance.Status + if models.InstanceIngStatusCheck(status) { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + result = database.DB.Model(&instance).Update("status", models.InstanceDeleting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerSaveError, iris.StatusInternalServerError) + return + } + + if status == models.InstanceRunning || status == models.InstancePaused { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.GpuCount)) + } + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.VolumeSize+30)) + + // TODO: Price calculation + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Delete, + Status: status, + InstanceID: instance.ID, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/instance/detail.go b/routers/api/v1/admin/instance/detail.go new file mode 100644 index 0000000..70dfbeb --- /dev/null +++ b/routers/api/v1/admin/instance/detail.go @@ -0,0 +1,53 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func detailHandler(ctx iris.Context) { + l.SetFunction("detailHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Select("id", "user_id", "server_id", "cpu_only", "gpu_count", "volume_size", "ssh_address", "ssh_passwd", "tensor_board_address", "grafana_address", "status", "image_name", "created_at").First(&instance) + if result.Error != nil { + l.Error("detail instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.Select("name", "gpu_type", "gpu_num").First(&server) + if result.Error != nil { + l.Error("detail server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + res := instanceStruct{ + Instances: instance, + GpuType: server.GpuType, + GpuNum: server.GpuNum, + GpuUsed: server.GpuUsed, + } + + middleware.Result(ctx, res) +} diff --git a/routers/api/v1/admin/instance/label.go b/routers/api/v1/admin/instance/label.go new file mode 100644 index 0000000..09ff22b --- /dev/null +++ b/routers/api/v1/admin/instance/label.go @@ -0,0 +1,49 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type labelReqStruct struct { + Label string `json:"label"` +} + +func labelHandler(ctx iris.Context) { + l.SetFunction("labelHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req labelReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Where(&instance).First(&instance) + if result.Error != nil { + l.Error("query instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceQueryError, iris.StatusInternalServerError) + return + } + + result = database.DB.Model(&instance).Update("label", req.Label) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceSaveError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/instance/list.go b/routers/api/v1/admin/instance/list.go new file mode 100644 index 0000000..0d0b226 --- /dev/null +++ b/routers/api/v1/admin/instance/list.go @@ -0,0 +1,88 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + var err error + + offset := 0 + limit := 20 + offsetStr := ctx.URLParam("offset") + limitStr := ctx.URLParam("limit") + + if offsetStr != "" { + offset, err = strconv.Atoi(offsetStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + if limitStr != "" { + limit, err = strconv.Atoi(limitStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + var total int64 + var instances []models.Instances + totalResult := database.DB.Model(&models.Instances{}).Count(&total) + if totalResult.Error != nil { + l.Error("list instances error: %v", totalResult.Error) + middleware.Error(ctx, middleware.CodeInstanceListError, iris.StatusInternalServerError) + return + } + + result := database.DB.Limit(limit).Offset(offset).Select("id", "user_id", "server_id", "cpu_only", "gpu_count", "volume_size", "ssh_address", "ssh_passwd", "jupyter_address", "tensor_board_address", "grafana_address", "status", "image_name", "label", "created_at").Order("id").Find(&instances) + if result.Error != nil { + l.Error("list instances error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceListError, iris.StatusInternalServerError) + return + } + + res := make([]instanceStruct, len(instances)) + for i, instance := range instances { + res[i] = instanceStruct{ + Instances: instance, + } + user := models.Users{ + ID: instance.UserID, + } + result := database.DB.Select("username").First(&user) + if result.Error == nil { + res[i].Username = user.Username + } else { + l.Error("query user %d error: %v", instance.UserID, result.Error) + } + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.Select("name", "gpu_type", "gpu_num", "cpu_count_per_gpu", "memory_per_gpu").First(&server) + if result.Error == nil { + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + res[i].ServerName = server.Name + res[i].GpuType = server.GpuType + res[i].GpuNum = server.GpuNum + res[i].GpuUsed = server.GpuUsed + res[i].CpuCountPerGpu = server.CpuCountPerGpu + res[i].MemoryPerGpu = server.MemoryPerGpu + } else { + l.Error("query server %d error: %v", instance.ServerID, result.Error) + } + } + + middleware.ResultWithTotal(ctx, res, total) +} diff --git a/routers/api/v1/admin/instance/modify.go b/routers/api/v1/admin/instance/modify.go new file mode 100644 index 0000000..f9fee12 --- /dev/null +++ b/routers/api/v1/admin/instance/modify.go @@ -0,0 +1,128 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type modifyReqStruct struct { + CpuOnly bool `json:"cpu_only"` + GpuCount *int `json:"gpu_count"` + VolumeSize *int `json:"volume_size"` +} + +func modifyHandler(ctx iris.Context) { + l.SetFunction("modifyHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req modifyReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if req.GpuCount != nil { + if *req.GpuCount < 0 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + if req.VolumeSize != nil { + if *req.VolumeSize < 50 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.First(&instance) + if result.Error != nil { + l.Error("query instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceQueryError, iris.StatusInternalServerError) + return + } + + if instance.Status != models.InstanceStopped { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.CpuOnly == instance.CpuOnly && req.CpuOnly { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if req.GpuCount != nil { + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-*req.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(*req.GpuCount)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + if req.VolumeSize != nil { + remainVolume, err := redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(instance.VolumeSize-*req.VolumeSize)).Result() + if err != nil { + l.Error("incrby volume size error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + if remainVolume < 0 { + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(*req.VolumeSize-instance.VolumeSize)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + + status := instance.Status + result = database.DB.Model(&instance).Update("status", models.InstanceModifying) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerSaveError, iris.StatusInternalServerError) + return + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Modify, + InstanceID: instance.ID, + Status: status, + + CpuOnly: req.CpuOnly, + GpuCount: req.GpuCount, + VolumeSize: req.VolumeSize, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/instance/routers.go b/routers/api/v1/admin/instance/routers.go new file mode 100644 index 0000000..5c4320b --- /dev/null +++ b/routers/api/v1/admin/instance/routers.go @@ -0,0 +1,37 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/logger" + + _logger "megrez/libs/logger" + + "github.com/kataras/iris/v12/core/router" +) + +var l *_logger.LoggerStruct + +type instanceStruct struct { + models.Instances + Username string `json:"username"` + ServerName string `json:"server_name"` + GpuType string `json:"gpu_type"` + GpuNum int `json:"gpu_num"` + GpuUsed int `json:"gpu_used"` + CpuCountPerGpu int `json:"cpu_count_per_gpu"` + MemoryPerGpu int `json:"memory_per_gpu"` +} + +func InitInstances(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.Admin.Instance") + + party.Get("/", listHandler) + party.Get("/{id:uint}", detailHandler) + party.Put("/{id:uint}", middleware.SuperAdminCheck, controlHandler) + party.Post("/", middleware.SuperAdminCheck, addHandler) + party.Post("/{id:uint}", middleware.SuperAdminCheck, modifyHandler) + party.Post("/{id:uint}/label", middleware.SuperAdminCheck, labelHandler) + party.Delete("/{id:uint}", middleware.SuperAdminCheck, deleteHandler) +} diff --git a/routers/api/v1/admin/routers.go b/routers/api/v1/admin/routers.go new file mode 100644 index 0000000..6ba9387 --- /dev/null +++ b/routers/api/v1/admin/routers.go @@ -0,0 +1,20 @@ +package admin + +import ( + "megrez/routers/api/v1/admin/images" + instances "megrez/routers/api/v1/admin/instance" + "megrez/routers/api/v1/admin/servers" + "megrez/routers/api/v1/admin/users" + "megrez/routers/api/v1/middleware" + + "github.com/kataras/iris/v12/core/router" +) + +func InitAdmin(party router.Party) { + party.Use(middleware.Auth, middleware.AuthCheck, middleware.AdminCheck) + + users.InitUser(party.Party("/users")) + servers.InitServer(party.Party("/servers")) + instances.InitInstances(party.Party("/instances")) + images.InitImages(party.Party("/images")) +} diff --git a/routers/api/v1/admin/servers/add.go b/routers/api/v1/admin/servers/add.go new file mode 100644 index 0000000..262a4d6 --- /dev/null +++ b/routers/api/v1/admin/servers/add.go @@ -0,0 +1,56 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func addHandler(ctx iris.Context) { + l.SetFunction("addHandler") + + var s serverStruct + if err := ctx.ReadJSON(&s); err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if s.Name == "" || s.IP == "" || s.Port == 0 || s.Apikey == "" || s.GpuType == "" || s.GpuNum == 0 || s.GpuDirverVersion == "" || s.GpuCudaVersion == "" || s.CpuCpuntPerGpu == 0 || s.MemoryPerGpu == 0 || s.VolumeTotal == 0 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + l.Debug("add server: %+v", s) + + server := models.Servers{ + Name: s.Name, + IP: s.IP, + Port: s.Port, + Apikey: s.Apikey, + GpuType: s.GpuType, + GpuNum: s.GpuNum, + GpuDriverVersion: s.GpuDirverVersion, + GpuCudaVersion: s.GpuCudaVersion, + + CpuCountPerGpu: s.CpuCpuntPerGpu, + MemoryPerGpu: s.MemoryPerGpu, + VolumeTotal: s.VolumeTotal, + Price: s.Price, + PriceVolume: s.PriceVolume, + } + result := database.DB.Create(&server) + if result.Error != nil { + l.Error("add server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerAddEditError, iris.StatusInternalServerError) + return + } + + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(server.GpuNum)) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(server.VolumeTotal)) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/servers/delete.go b/routers/api/v1/admin/servers/delete.go new file mode 100644 index 0000000..23993ec --- /dev/null +++ b/routers/api/v1/admin/servers/delete.go @@ -0,0 +1,43 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func deleteHandler(ctx iris.Context) { + l.SetFunction("deleteHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var instances []models.Instances + result := database.DB.Where("server_id = ?", id).Find(&instances) + if result.Error != nil { + l.Error("get instances error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerDeleteError, iris.StatusInternalServerError) + return + } + if len(instances) > 0 { + middleware.Error(ctx, middleware.CodeAdminServerInstanceError, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: id, + } + result = database.DB.Delete(&server) + if result.Error != nil { + l.Error("delete server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerDeleteError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/servers/detail.go b/routers/api/v1/admin/servers/detail.go new file mode 100644 index 0000000..f5a0025 --- /dev/null +++ b/routers/api/v1/admin/servers/detail.go @@ -0,0 +1,39 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func detailHandler(ctx iris.Context) { + l.SetFunction("detailHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: id, + } + result := database.DB.First(&server) + if result.Error != nil { + l.Error("detail server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerDetailError, iris.StatusInternalServerError) + return + } + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(id))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(id))).Scan(&server.VolumeUsed) + server.VolumeUsed = server.VolumeTotal - server.VolumeUsed + + middleware.Result(ctx, server) +} diff --git a/routers/api/v1/admin/servers/list.go b/routers/api/v1/admin/servers/list.go new file mode 100644 index 0000000..35727fd --- /dev/null +++ b/routers/api/v1/admin/servers/list.go @@ -0,0 +1,62 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + var err error + + offset := 0 + limit := 20 + offsetStr := ctx.URLParam("offset") + limitStr := ctx.URLParam("limit") + + if offsetStr != "" { + offset, err = strconv.Atoi(offsetStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + if limitStr != "" { + limit, err = strconv.Atoi(limitStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + var total int64 + var servers []models.Servers + totalResult := database.DB.Model(&models.Servers{}).Count(&total) + if totalResult.Error != nil { + l.Error("list servers error: %v", totalResult.Error) + middleware.Error(ctx, middleware.CodeAdminServerListError, iris.StatusInternalServerError) + return + } + + result := database.DB.Limit(limit).Offset(offset).Select("id", "name", "ip", "gpu_type", "gpu_num", "gpu_driver_version", "gpu_cuda_version", "cpu_count_per_gpu", "memory_per_gpu", "volume_total", "price", "price_volume", "gpu_used", "volume_used", "created_at").Order("id").Find(&servers) + if result.Error != nil { + l.Error("list servers error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerListError, iris.StatusInternalServerError) + return + } + + for i, server := range servers { + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID))).Scan(&servers[i].GpuUsed) + servers[i].GpuUsed = server.GpuNum - servers[i].GpuUsed + + redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID))).Scan(&servers[i].VolumeUsed) + servers[i].VolumeUsed = server.VolumeTotal - servers[i].VolumeUsed + } + + middleware.ResultWithTotal(ctx, servers, total) +} diff --git a/routers/api/v1/admin/servers/modify.go b/routers/api/v1/admin/servers/modify.go new file mode 100644 index 0000000..7d0ce56 --- /dev/null +++ b/routers/api/v1/admin/servers/modify.go @@ -0,0 +1,84 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func modifyHandler(ctx iris.Context) { + l.SetFunction("modifyHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req serverStruct + if err := ctx.ReadJSON(&req); err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: id, + } + result := database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerAddEditError, iris.StatusInternalServerError) + return + } + + if req.Name != "" && req.Name != server.Name { + server.Name = req.Name + } + if req.IP != "" && req.IP != server.IP { + server.IP = req.IP + } + if req.Port != 0 && req.Port != server.Port { + server.Port = req.Port + } + if req.Apikey != "" && req.Apikey != server.Apikey { + server.Apikey = req.Apikey + } + if req.GpuType != "" && req.GpuType != server.GpuType { + server.GpuType = req.GpuType + } + if req.GpuNum != 0 && req.GpuNum != server.GpuNum { + server.GpuNum = req.GpuNum + } + if req.GpuDirverVersion != "" && req.GpuDirverVersion != server.GpuDriverVersion { + server.GpuDriverVersion = req.GpuDirverVersion + } + if req.GpuCudaVersion != "" && req.GpuCudaVersion != server.GpuCudaVersion { + server.GpuCudaVersion = req.GpuCudaVersion + } + if req.CpuCpuntPerGpu != 0 && req.CpuCpuntPerGpu != server.CpuCountPerGpu { + server.CpuCountPerGpu = req.CpuCpuntPerGpu + } + if req.MemoryPerGpu != 0 && req.MemoryPerGpu != server.MemoryPerGpu { + server.MemoryPerGpu = req.MemoryPerGpu + } + if req.VolumeTotal != 0 && req.VolumeTotal != server.VolumeTotal { + server.VolumeTotal = req.VolumeTotal + } + if req.Price != 0 && req.Price != server.Price { + server.Price = req.Price + } + if req.PriceVolume != 0 && req.PriceVolume != server.PriceVolume { + server.PriceVolume = req.PriceVolume + } + + result = database.DB.Save(&server) + if result.Error != nil { + l.Error("save server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerAddEditError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/servers/routers.go b/routers/api/v1/admin/servers/routers.go new file mode 100644 index 0000000..2f3f8f3 --- /dev/null +++ b/routers/api/v1/admin/servers/routers.go @@ -0,0 +1,40 @@ +package servers + +import ( + "github.com/kataras/iris/v12/core/router" + + "megrez/routers/api/v1/middleware" + "megrez/services/logger" + + _logger "megrez/libs/logger" +) + +var l *_logger.LoggerStruct + +type serverStruct struct { + Name string `json:"name,omitempty"` + IP string `json:"ip,omitempty"` + Port int `json:"port,omitempty"` + Apikey string `json:"apikey,omitempty"` + GpuType string `json:"gpu_type,omitempty"` + GpuNum int `json:"gpu_num,omitempty"` + GpuDirverVersion string `json:"gpu_driver_version,omitempty"` + GpuCudaVersion string `json:"gpu_cuda_version,omitempty"` + + CpuCpuntPerGpu int `json:"cpu_count_per_gpu,omitempty"` + MemoryPerGpu int `json:"memory_per_gpu,omitempty"` // Unit `GB` + VolumeTotal int `json:"volume_total,omitempty"` // Unit `GB` + Price float64 `json:"price,omitempty"` // 1 GPU Per Hour + PriceVolume float64 `json:"price_volume,omitempty"` // 1GB Per Hour +} + +func InitServer(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.Admin.Servers") + + party.Get("/", listHandler) + party.Get("/{id:uint}", detailHandler) + party.Post("/", middleware.SuperAdminCheck, addHandler) + party.Post("/{id:uint}", middleware.SuperAdminCheck, modifyHandler) + party.Delete("/{id:uint}", middleware.SuperAdminCheck, deleteHandler) +} diff --git a/routers/api/v1/admin/users/add.go b/routers/api/v1/admin/users/add.go new file mode 100644 index 0000000..82abcb9 --- /dev/null +++ b/routers/api/v1/admin/users/add.go @@ -0,0 +1 @@ +package users diff --git a/routers/api/v1/admin/users/delete.go b/routers/api/v1/admin/users/delete.go new file mode 100644 index 0000000..505bce1 --- /dev/null +++ b/routers/api/v1/admin/users/delete.go @@ -0,0 +1,49 @@ +package users + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func deleteHandler(ctx iris.Context) { + l.SetFunction("deleteHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if id == 1 { + middleware.Error(ctx, middleware.CodeAdminUserDeleteError, iris.StatusBadRequest) + return + } + + var instances []models.Instances + result := database.DB.Where("user_id = ?", id).Find(&instances) + if result.Error != nil { + l.Error("get user instances error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminUserDeleteError, iris.StatusInternalServerError) + return + } + + if len(instances) > 0 { + middleware.Error(ctx, middleware.CodeAdminUserInstanceNoEmpty, iris.StatusBadRequest) + return + } + + user := models.Users{ + ID: id, + } + result = database.DB.Delete(&user) + if result.Error != nil { + l.Error("delete user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminUserDeleteError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/users/detail.go b/routers/api/v1/admin/users/detail.go new file mode 100644 index 0000000..adc1392 --- /dev/null +++ b/routers/api/v1/admin/users/detail.go @@ -0,0 +1,31 @@ +package users + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func detailHandler(ctx iris.Context) { + l.SetFunction("detailHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + user := models.Users{ + ID: id, + } + result := database.DB.Select("id", "username", "email", "role", "balance", "created_at").First(&user) + if result.Error != nil { + l.Error("detail user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminUserDetailError, iris.StatusInternalServerError) + return + } + + middleware.Result(ctx, user) +} diff --git a/routers/api/v1/admin/users/list.go b/routers/api/v1/admin/users/list.go new file mode 100644 index 0000000..e8a9ea2 --- /dev/null +++ b/routers/api/v1/admin/users/list.go @@ -0,0 +1,53 @@ +package users + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "strconv" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + var err error + + offset := 0 + limit := 20 + offsetStr := ctx.URLParam("offset") + limitStr := ctx.URLParam("limit") + + if offsetStr != "" { + offset, err = strconv.Atoi(offsetStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + if limitStr != "" { + limit, err = strconv.Atoi(limitStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + var total int64 + var users []models.Users + totalResult := database.DB.Model(&models.Users{}).Count(&total) + if totalResult.Error != nil { + l.Error("list users error: %v", totalResult.Error) + middleware.Error(ctx, middleware.CodeAdminUserListError, iris.StatusInternalServerError) + return + } + + result := database.DB.Limit(limit).Offset(offset).Select("id", "username", "email", "role", "balance", "created_at").Order("id").Find(&users) + if result.Error != nil { + l.Error("list users error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminUserListError, iris.StatusInternalServerError) + return + } + + middleware.ResultWithTotal(ctx, users, total) +} diff --git a/routers/api/v1/admin/users/modify.go b/routers/api/v1/admin/users/modify.go new file mode 100644 index 0000000..32aa034 --- /dev/null +++ b/routers/api/v1/admin/users/modify.go @@ -0,0 +1,65 @@ +package users + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type modifyReqStruct struct { + Password *string `json:"password"` + Role *int `json:"role"` +} + +func modifyHandler(ctx iris.Context) { + l.SetFunction("modifyHandler") + + userId, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req modifyReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if userId == 1 { + if req.Role != nil { + middleware.Error(ctx, middleware.CodeAdminUserModifyError, iris.StatusBadRequest) + return + } + } + + user := models.Users{ + ID: userId, + } + result := database.DB.Where(&user).First(&user) + if result.Error != nil { + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + if req.Password != nil { + if *req.Password != "" { + user.Password = user.PasswordHash(*req.Password) + } + } + + if req.Role != nil { + user.Role = *req.Role + } + + result = database.DB.Save(&user) + if result.Error != nil { + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/admin/users/routers.go b/routers/api/v1/admin/users/routers.go new file mode 100644 index 0000000..c24d690 --- /dev/null +++ b/routers/api/v1/admin/users/routers.go @@ -0,0 +1,22 @@ +package users + +import ( + _logger "megrez/libs/logger" + "megrez/routers/api/v1/middleware" + "megrez/services/logger" + + "github.com/kataras/iris/v12/core/router" +) + +var l *_logger.LoggerStruct + +func InitUser(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.Admin.Users") + + party.Get("/", listHandler) + party.Get("/{id:uint}", detailHandler) + // party.Post("/", addHandler) + party.Post("/{id:uint}", middleware.SuperAdminCheck, modifyHandler) + party.Delete("/{id:uint}", middleware.SuperAdminCheck, deleteHandler) +} diff --git a/routers/api/v1/middleware/code.go b/routers/api/v1/middleware/code.go new file mode 100644 index 0000000..730496d --- /dev/null +++ b/routers/api/v1/middleware/code.go @@ -0,0 +1,87 @@ +package middleware + +type ResCode int + +const ( + CodeSuccess ResCode = 200 + CodeServeBusy ResCode = 500 + CodeBadRequest ResCode = 400 + CodeUnauthorized ResCode = 401 + CodeForbidden ResCode = 403 + + CodePasswordError ResCode = 1000 + CodeLoginError ResCode = 1001 + CodeUserNotExist ResCode = 1002 + CodeRegisterRequestError ResCode = 1003 + CodeRegisterError ResCode = 1004 + CodeInternalCreateError ResCode = 1010 + CodeInstanceDeleteError ResCode = 1011 + CodeInstanceQueryError ResCode = 1012 + CodeInternalPatchError ResCode = 1013 + CodeInstanceListError ResCode = 1014 + CodeInstanceDetailError ResCode = 1015 + CodeInstanceStartError ResCode = 1016 + CodeInstancePauseError ResCode = 1017 + CodeInstanceStopError ResCode = 1018 + CodeInstanceRestartError ResCode = 1019 + CodeInstanceStatusError ResCode = 1020 + CodeInstanceSaveError ResCode = 1021 + + CodeServerQueryError ResCode = 1031 + CodeServerSaveError ResCode = 1032 + CodeResourceInsufficient ResCode = 1040 + + CodeAdminServerAddEditError ResCode = 2001 + CodeAdminServerListError ResCode = 2002 + CodeAdminServerDetailError ResCode = 2003 + CodeAdminServerDeleteError ResCode = 2004 + CodeAdminServerInstanceError ResCode = 2005 + CodeAdminUserQueryError ResCode = 2010 + CodeAdminUserListError ResCode = 2011 + CodeAdminUserDetailError ResCode = 2012 + CodeAdminUserDeleteError ResCode = 2013 + CodeAdminUserInstanceNoEmpty ResCode = 2014 + CodeAdminUserModifyError ResCode = 2015 +) + +var codeMsgMap = map[ResCode]string{ + CodeSuccess: "success", + CodeServeBusy: "server busy", + CodeUnauthorized: "unauthorized", + CodeBadRequest: "bad request", + CodeForbidden: "forbidden", + + CodePasswordError: "password error", + CodeLoginError: "login error", + CodeUserNotExist: "user not exist", + CodeRegisterRequestError: "register request error", + CodeRegisterError: "username or email exist", + CodeInternalCreateError: "create error", + CodeInstanceDeleteError: "delete instance error", + CodeInstanceStatusError: "instance status error", + CodeInstanceQueryError: "query instance error", + CodeInstanceListError: "list instance error", + CodeInstanceDetailError: "detail instance error", + CodeInstanceStartError: "start instance error", + CodeInstancePauseError: "pause instance error", + CodeInstanceStopError: "stop instance error", + CodeInstanceRestartError: "restart instance error", + CodeResourceInsufficient: "resource insufficient", + CodeInternalPatchError: "patch error", + CodeInstanceSaveError: "save instance error", + + CodeServerQueryError: "query server error", + CodeServerSaveError: "save server error", + + CodeAdminServerAddEditError: "add server error", + CodeAdminServerListError: "list server error", + CodeAdminServerDetailError: "detail server error", + CodeAdminServerDeleteError: "delete server error", + CodeAdminServerInstanceError: "server instances not empty", + CodeAdminUserQueryError: "query user error", + CodeAdminUserListError: "list user error", + CodeAdminUserDetailError: "detail user error", + CodeAdminUserDeleteError: "delete user error", + CodeAdminUserModifyError: "modify user error", + CodeAdminUserInstanceNoEmpty: "user instances not empty", +} diff --git a/routers/api/v1/middleware/middleware.go b/routers/api/v1/middleware/middleware.go new file mode 100644 index 0000000..7dd99fb --- /dev/null +++ b/routers/api/v1/middleware/middleware.go @@ -0,0 +1,71 @@ +package middleware + +import ( + "megrez/routers/api/v1/sessions" + "megrez/services/redis" + + "github.com/kataras/iris/v12" +) + +func Auth(ctx iris.Context) { + sess := sessions.Session() + sess.UseDatabase(redis.DB) + session := sess.Start(ctx) + + auth, _ := session.GetBoolean("authenticated") + if !auth { + ctx.Values().Set("authenticated", false) + ctx.Next() + return + } else { + ctx.Values().Set("authenticated", true) + } + + userId, _ := session.GetInt("userId") + ctx.Values().Set("userId", userId) + ctx.Values().Set("email", session.GetString("email")) + + role, _ := session.GetInt("role") + ctx.Values().Set("role", role) + + ctx.Next() +} + +func AuthCheck(ctx iris.Context) { + if ctx.Values().Get("authenticated") == nil || !ctx.Values().Get("authenticated").(bool) { + Error(ctx, CodeUnauthorized, iris.StatusUnauthorized) + return + } + + ctx.Next() +} + +func UserCheck(ctx iris.Context) { + usertype, _ := ctx.Values().GetInt("role") + if usertype < 1 { + Error(ctx, CodeForbidden, iris.StatusForbidden) + return + } + + ctx.Next() +} + +func AdminCheck(ctx iris.Context) { + usertype, _ := ctx.Values().GetInt("role") + if usertype < 2 { + Error(ctx, CodeForbidden, iris.StatusForbidden) + return + } + + ctx.Next() +} + +func SuperAdminCheck(ctx iris.Context) { + usertype, _ := ctx.Values().GetInt("role") + if usertype < 3 { + Error(ctx, CodeForbidden, iris.StatusForbidden) + return + } + + ctx.Next() +} diff --git a/routers/api/v1/middleware/response.go b/routers/api/v1/middleware/response.go new file mode 100644 index 0000000..556a550 --- /dev/null +++ b/routers/api/v1/middleware/response.go @@ -0,0 +1,41 @@ +package middleware + +import "github.com/kataras/iris/v12" + +func Success(ctx iris.Context) { + ctx.JSON(Response{ + Code: CodeSuccess, + Msg: codeMsgMap[CodeSuccess], + Data: nil, + }) +} + +func Result(ctx iris.Context, data any) { + ctx.JSON(Response{ + Code: CodeSuccess, + Msg: codeMsgMap[CodeSuccess], + Data: &Data{ + Result: data, + }, + }) +} + +func ResultWithTotal(ctx iris.Context, data any, total int64) { + ctx.JSON(Response{ + Code: CodeSuccess, + Msg: codeMsgMap[CodeSuccess], + Data: &Data{ + Result: data, + Total: total, + }, + }) +} + +func Error(ctx iris.Context, code ResCode, statusCode int) { + ctx.StatusCode(statusCode) + ctx.JSON(Response{ + Code: code, + Msg: codeMsgMap[code], + Data: nil, + }) +} diff --git a/routers/api/v1/middleware/struct.go b/routers/api/v1/middleware/struct.go new file mode 100644 index 0000000..c0466c6 --- /dev/null +++ b/routers/api/v1/middleware/struct.go @@ -0,0 +1,12 @@ +package middleware + +type Response struct { + Code ResCode `json:"code"` + Msg string `json:"msg"` + Data *Data `json:"data"` +} + +type Data struct { + Result any `json:"result"` + Total int64 `json:"total,omitempty"` +} diff --git a/routers/api/v1/routers.go b/routers/api/v1/routers.go new file mode 100644 index 0000000..f8b54ae --- /dev/null +++ b/routers/api/v1/routers.go @@ -0,0 +1,13 @@ +package v1 + +import ( + "megrez/routers/api/v1/admin" + "megrez/routers/api/v1/user" + + "github.com/kataras/iris/v12/core/router" +) + +func InitApiV1(party router.Party) { + admin.InitAdmin(party.Party("/admin")) + user.InitUser(party.Party("/user")) +} diff --git a/routers/api/v1/sessions/session.go b/routers/api/v1/sessions/session.go new file mode 100644 index 0000000..172901c --- /dev/null +++ b/routers/api/v1/sessions/session.go @@ -0,0 +1,14 @@ +package sessions + +import ( + "time" + + "github.com/kataras/iris/v12/sessions" +) + +func Session() *sessions.Sessions { + return sessions.New(sessions.Config{ + Cookie: "session_id", + Expires: 7 * 24 * time.Hour, // 7 Days + }) +} diff --git a/routers/api/v1/user/images/list.go b/routers/api/v1/user/images/list.go new file mode 100644 index 0000000..bd89919 --- /dev/null +++ b/routers/api/v1/user/images/list.go @@ -0,0 +1,35 @@ +package images + +import ( + "encoding/json" + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + + res := make(map[string]string) + + system := models.System{ + Key: imagesKey, + } + result := database.DB.First(&system) + if result.Error != nil { + l.Error("get system error: %v", result.Error) + middleware.Result(ctx, res) + return + } + + err := json.Unmarshal([]byte(system.Value), &res) + if err != nil { + l.Error("unmarshal system error: %v", err) + middleware.Error(ctx, middleware.CodeServeBusy, iris.StatusInternalServerError) + return + } + + middleware.Result(ctx, res) +} diff --git a/routers/api/v1/user/images/routers.go b/routers/api/v1/user/images/routers.go new file mode 100644 index 0000000..0574297 --- /dev/null +++ b/routers/api/v1/user/images/routers.go @@ -0,0 +1,20 @@ +package images + +import ( + "megrez/services/logger" + + "github.com/kataras/iris/v12/core/router" + + _logger "megrez/libs/logger" +) + +const imagesKey = "images" + +var l *_logger.LoggerStruct + +func InitImages(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.User.Images") + + party.Get("/", listHandler) +} diff --git a/routers/api/v1/user/instances/add.go b/routers/api/v1/user/instances/add.go new file mode 100644 index 0000000..0ddf6c5 --- /dev/null +++ b/routers/api/v1/user/instances/add.go @@ -0,0 +1,99 @@ +package instances + +import ( + "megrez/libs/crypto" + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type addReqStruct struct { + ServerID uint `json:"server_id"` + + ImageName string `json:"image_name"` + GpuCount int `json:"gpu_count"` + VolumeSize int `json:"volume_size"` +} + +func addHandler(ctx iris.Context) { + l.SetFunction("addHandler") + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req addReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if req.ServerID == 0 || req.ImageName == "" || req.GpuCount <= 0 || req.VolumeSize < 50 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + } + + server := models.Servers{ + ID: req.ServerID, + } + result := database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-req.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + remainVolume, err := redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(-req.VolumeSize-30)).Result() + if err != nil { + l.Error("incrby volume size error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 || remainVolume < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(req.GpuCount)) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(req.VolumeSize+30)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + UserID: uint(userId), + ServerID: req.ServerID, + ImageName: req.ImageName, + GpuCount: req.GpuCount, + VolumeSize: req.VolumeSize, + + SshPasswd: crypto.Hex(16), + + Status: models.InstanceReady, + } + result = database.DB.Create(&instance) + if result.Error != nil { + l.Error("create instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInternalCreateError, iris.StatusInternalServerError) + return + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Add, + InstanceID: instance.ID, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/instances/control.go b/routers/api/v1/user/instances/control.go new file mode 100644 index 0000000..cd7d1ac --- /dev/null +++ b/routers/api/v1/user/instances/control.go @@ -0,0 +1,141 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type controlStruct struct { + Action instanceController.Action `json:"action"` // 1: start, 2: pause , 3: stop, 4: restart +} + +func controlHandler(ctx iris.Context) { + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req controlStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Where("id = ?", id).Where("user_id = ?", userId).First(&instance) + if result.Error != nil { + l.Error("detail instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + if models.InstanceIngStatusCheck(instance.Status) { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionStop && instance.Status != models.InstanceRunning && instance.Status != models.InstancePaused { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionPause && instance.Status != models.InstanceRunning { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.Action == instanceController.ActionStart && instance.Status != models.InstanceStopped && instance.Status != models.InstancePaused { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + status := instance.Status + if status == models.InstanceStopped && (req.Action == instanceController.ActionStart || req.Action == instanceController.ActionRestart) { + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-instance.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(instance.GpuCount)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + + switch req.Action { + case instanceController.ActionStart: + result = database.DB.Model(&instance).Update("status", models.InstanceStarting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceStartError, iris.StatusInternalServerError) + return + } + + case instanceController.ActionPause: + result = database.DB.Model(&instance).Update("status", models.InstancePausing) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstancePauseError, iris.StatusInternalServerError) + return + } + + case instanceController.ActionStop: + result = database.DB.Model(&instance).Update("status", models.InstanceStopping) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceStopError, iris.StatusInternalServerError) + return + } + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(instance.GpuCount)) + + case instanceController.ActionRestart: + result = database.DB.Model(&instance).Update("status", models.InstanceRestarting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceRestartError, iris.StatusInternalServerError) + return + } + + default: + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Control, + InstanceID: instance.ID, + Status: status, + Action: req.Action, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/instances/delete.go b/routers/api/v1/user/instances/delete.go new file mode 100644 index 0000000..1867ff5 --- /dev/null +++ b/routers/api/v1/user/instances/delete.go @@ -0,0 +1,65 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func deleteHandler(ctx iris.Context) { + l.SetFunction("deleteHandler") + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var instance models.Instances + result := database.DB.Where("id = ?", id).Where("user_id = ?", userId).First(&instance) + if result.Error != nil { + l.Error("get instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDeleteError, iris.StatusInternalServerError) + return + } + + status := instance.Status + if models.InstanceIngStatusCheck(status) { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + result = database.DB.Model(&instance).Update("status", models.InstanceDeleting) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerSaveError, iris.StatusInternalServerError) + return + } + + if status == models.InstanceRunning || status == models.InstancePaused { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.GpuCount)) + } + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(instance.ServerID)), int64(instance.VolumeSize+30)) + + // TODO: Price calculation + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Delete, + Status: status, + InstanceID: instance.ID, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/instances/detail.go b/routers/api/v1/user/instances/detail.go new file mode 100644 index 0000000..efda447 --- /dev/null +++ b/routers/api/v1/user/instances/detail.go @@ -0,0 +1,61 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func detailHandler(ctx iris.Context) { + l.SetFunction("detailHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Where("id = ?", id).Where("user_id = ?", userId).Select("id", "server_id", "cpu_only", "gpu_count", "volume_size", "ssh_address", "ssh_passwd", "tensor_board_address", "grafana_address", "status", "image_name", "created_at").First(&instance) + if result.Error != nil { + l.Error("detail instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.Select("name", "gpu_type", "gpu_num", "cpu_count_per_gpu", "memory_per_gpu").First(&server) + if result.Error != nil { + l.Error("detail server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceDetailError, iris.StatusInternalServerError) + return + } + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + res := instanceStruct{ + Instances: instance, + GpuType: server.GpuType, + GpuNum: server.GpuNum, + GpuUsed: server.GpuUsed, + CpuCountPerGpu: server.CpuCountPerGpu, + MemoryPerGpu: server.MemoryPerGpu, + } + + middleware.Result(ctx, res) +} diff --git a/routers/api/v1/user/instances/label.go b/routers/api/v1/user/instances/label.go new file mode 100644 index 0000000..61f3bf9 --- /dev/null +++ b/routers/api/v1/user/instances/label.go @@ -0,0 +1,55 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type labelReqStruct struct { + Label string `json:"label"` +} + +func labelHandler(ctx iris.Context) { + l.SetFunction("labelHandler") + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req labelReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Where(&instance).Where("user_id = ?", userId).First(&instance) + if result.Error != nil { + l.Error("query instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceQueryError, iris.StatusInternalServerError) + return + } + + result = database.DB.Model(&instance).Update("label", req.Label) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceSaveError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/instances/list.go b/routers/api/v1/user/instances/list.go new file mode 100644 index 0000000..736ac11 --- /dev/null +++ b/routers/api/v1/user/instances/list.go @@ -0,0 +1,85 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("listHandler") + var err error + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + offset := 0 + limit := 20 + offsetStr := ctx.URLParam("offset") + limitStr := ctx.URLParam("limit") + + if offsetStr != "" { + offset, err = strconv.Atoi(offsetStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + if limitStr != "" { + limit, err = strconv.Atoi(limitStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + var total int64 + var instances []models.Instances + totalResult := database.DB.Model(&models.Instances{}).Where("user_id = ?", userId).Count(&total) + if totalResult.Error != nil { + l.Error("list instances error: %v", totalResult.Error) + middleware.Error(ctx, middleware.CodeInstanceListError, iris.StatusInternalServerError) + return + } + + result := database.DB.Where("user_id = ?", userId).Limit(limit).Offset(offset).Select("id", "server_id", "cpu_only", "gpu_count", "volume_size", "ssh_address", "ssh_passwd", "jupyter_address", "tensor_board_address", "grafana_address", "status", "image_name", "label", "created_at").Order("id").Find(&instances) + if result.Error != nil { + l.Error("list instances error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceListError, iris.StatusInternalServerError) + return + } + + res := make([]instanceStruct, len(instances)) + for i, instance := range instances { + res[i] = instanceStruct{ + Instances: instance, + } + server := models.Servers{ + ID: instance.ServerID, + } + result := database.DB.Select("name", "gpu_type", "gpu_num", "cpu_count_per_gpu", "memory_per_gpu").First(&server) + if result.Error != nil { + l.Error("query server %d error: %v", instance.ServerID, result.Error) + continue + } + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(instance.ServerID))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + res[i].ServerName = server.Name + res[i].GpuType = server.GpuType + res[i].GpuNum = server.GpuNum + res[i].GpuUsed = server.GpuUsed + res[i].CpuCountPerGpu = server.CpuCountPerGpu + res[i].MemoryPerGpu = server.MemoryPerGpu + } + + middleware.ResultWithTotal(ctx, res, total) +} diff --git a/routers/api/v1/user/instances/modify.go b/routers/api/v1/user/instances/modify.go new file mode 100644 index 0000000..dbe32e8 --- /dev/null +++ b/routers/api/v1/user/instances/modify.go @@ -0,0 +1,134 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/dispatcher" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +type modifyReqStruct struct { + CpuOnly bool `json:"cpu_only"` + GpuCount *int `json:"gpu_count"` + VolumeSize *int `json:"volume_size"` +} + +func modifyHandler(ctx iris.Context) { + l.SetFunction("modifyHandler") + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var req modifyReqStruct + err = ctx.ReadJSON(&req) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if req.GpuCount != nil { + if *req.GpuCount < 0 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + if req.VolumeSize != nil { + if *req.VolumeSize < 50 { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + instance := models.Instances{ + ID: id, + } + result := database.DB.Where(&instance).Where("user_id = ?", userId).First(&instance) + if result.Error != nil { + l.Error("query instance error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInstanceQueryError, iris.StatusInternalServerError) + return + } + + if instance.Status != models.InstanceStopped { + middleware.Error(ctx, middleware.CodeInstanceStatusError, iris.StatusBadRequest) + return + } + + if req.CpuOnly == instance.CpuOnly && req.CpuOnly { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if req.GpuCount != nil { + remainGpu, err := redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(-*req.GpuCount)).Result() + if err != nil { + l.Error("incrby gpu num error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + + if remainGpu < 0 { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(*req.GpuCount)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + if req.VolumeSize != nil { + remainVolume, err := redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(instance.VolumeSize-*req.VolumeSize)).Result() + if err != nil { + l.Error("incrby volume size error: %v", err) + middleware.Error(ctx, middleware.CodeServerQueryError, iris.StatusInternalServerError) + return + } + if remainVolume < 0 { + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(*req.VolumeSize-instance.VolumeSize)) + middleware.Error(ctx, middleware.CodeResourceInsufficient, iris.StatusBadRequest) + return + } + } + + status := instance.Status + result = database.DB.Model(&instance).Update("status", models.InstanceModifying) + if result.Error != nil { + l.Error("update instance status error: %v", result.Error) + middleware.Error(ctx, middleware.CodeServerSaveError, iris.StatusInternalServerError) + return + } + + dispatcherData := dispatcher.Data{ + Type: dispatcher.Modify, + InstanceID: instance.ID, + Status: status, + + CpuOnly: req.CpuOnly, + GpuCount: req.GpuCount, + VolumeSize: req.VolumeSize, + } + dispatcher.Push(instance.ServerID, dispatcherData) + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/instances/routers.go b/routers/api/v1/user/instances/routers.go new file mode 100644 index 0000000..b2b2e78 --- /dev/null +++ b/routers/api/v1/user/instances/routers.go @@ -0,0 +1,38 @@ +package instances + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/logger" + + _logger "megrez/libs/logger" + + "github.com/kataras/iris/v12/core/router" +) + +var l *_logger.LoggerStruct + +type instanceStruct struct { + models.Instances + ServerName string `json:"server_name"` + GpuType string `json:"gpu_type"` + GpuNum int `json:"gpu_num"` + GpuUsed int `json:"gpu_used"` + CpuCountPerGpu int `json:"cpu_count_per_gpu"` + MemoryPerGpu int `json:"memory_per_gpu"` +} + +func InitInstances(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.User.Instance") + + party.Use(middleware.AuthCheck, middleware.UserCheck) + + party.Get("/", listHandler) + party.Get("/{id:uint}", detailHandler) + party.Put("/{id:uint}", controlHandler) + party.Post("/", addHandler) + party.Post("/{id:uint}", modifyHandler) + party.Post("/{id:uint}/label", labelHandler) + party.Delete("/{id:uint}", deleteHandler) +} diff --git a/routers/api/v1/user/login.go b/routers/api/v1/user/login.go new file mode 100644 index 0000000..4d5852e --- /dev/null +++ b/routers/api/v1/user/login.go @@ -0,0 +1,71 @@ +package user + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/routers/api/v1/sessions" + "megrez/services/database" + "megrez/services/redis" + "time" + + "github.com/kataras/iris/v12" +) + +type userLoginStruct struct { + Account string `json:"account"` + Password string `json:"password"` + // HCaptchaToken string + // CaptchaType string + // Key string +} + +func loginHandler(ctx iris.Context) { + sess := sessions.Session() + session := sess.Start(ctx) + sess.UseDatabase(redis.DB) + + var userReq userLoginStruct + if err := ctx.ReadJSON(&userReq); err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if userReq.Account == "" || userReq.Password == "" { + middleware.Error(ctx, middleware.CodeLoginError, iris.StatusBadRequest) + return + } + + user := models.Users{} + result := database.DB.Where("username = ?", userReq.Account).Or("email = ?", userReq.Account).First(&user) + if result.Error != nil { + middleware.Error(ctx, middleware.CodeLoginError, iris.StatusBadRequest) + return + } + + if !user.CheckPassword(userReq.Password) { + middleware.Error(ctx, middleware.CodeLoginError, iris.StatusBadRequest) + return + } + + if user.Role < 1 { + middleware.Error(ctx, middleware.CodeForbidden, iris.StatusForbidden) + return + } + + session.Set("authenticated", true) + session.Set("userId", user.ID) + session.Set("username", user.Username) + session.Set("email", user.Email) + session.Set("role", user.Role) + session.Set("loginTime", time.Now().Unix()) + + profile := profile{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Role: user.Role, + Balance: user.Balance, + } + + middleware.Result(ctx, profile) +} diff --git a/routers/api/v1/user/logout.go b/routers/api/v1/user/logout.go new file mode 100644 index 0000000..e586987 --- /dev/null +++ b/routers/api/v1/user/logout.go @@ -0,0 +1,26 @@ +package user + +import ( + "megrez/routers/api/v1/middleware" + "megrez/routers/api/v1/sessions" + "megrez/services/redis" + "net/http" + "time" + + "github.com/kataras/iris/v12" +) + +func logoutHandler(ctx iris.Context) { + sess := sessions.Session() + session := sess.Start(ctx) + sess.UseDatabase(redis.DB) + sessionId := session.ID() + + session.Destroy() + ctx.SetCookie(&http.Cookie{ + Name: "session_id", + Value: sessionId, + Expires: time.Now(), + }) + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/orders.go b/routers/api/v1/user/orders.go new file mode 100644 index 0000000..a00006b --- /dev/null +++ b/routers/api/v1/user/orders.go @@ -0,0 +1 @@ +package user diff --git a/routers/api/v1/user/profile.go b/routers/api/v1/user/profile.go new file mode 100644 index 0000000..934ee64 --- /dev/null +++ b/routers/api/v1/user/profile.go @@ -0,0 +1,46 @@ +package user + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type profile struct { + ID uint `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Role int `json:"role"` + Balance float64 `json:"balance"` +} + +func profileHandler(ctx iris.Context) { + l.SetFunction("profileHandler") + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + user := models.Users{ + ID: uint(userId), + } + result := database.DB.First(&user) + if result.Error != nil { + l.Error("get user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeUserNotExist, iris.StatusInternalServerError) + return + } + + profile := profile{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Role: user.Role, + Balance: user.Balance, + } + + middleware.Result(ctx, profile) +} diff --git a/routers/api/v1/user/register.go b/routers/api/v1/user/register.go new file mode 100644 index 0000000..9235ed7 --- /dev/null +++ b/routers/api/v1/user/register.go @@ -0,0 +1,52 @@ +package user + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/config" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type registerStruct struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` +} + +func registerHandler(ctx iris.Context) { + l.SetFunction("registerHandler") + + var userReq registerStruct + if err := ctx.ReadJSON(&userReq); err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + if userReq.Username == "" || userReq.Email == "" || userReq.Password == "" { + middleware.Error(ctx, middleware.CodeRegisterRequestError, iris.StatusBadRequest) + return + } + + user := models.Users{ + Username: userReq.Username, + Email: userReq.Email, + Role: 0, + Balance: 0, + } + user.Password = user.PasswordHash(userReq.Password) + + if !config.GetSystemVerify() { + user.Role = 1 + } + + result := database.DB.Create(&user) + if result.Error != nil { + l.Error("create user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeRegisterError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/resetPassword.go b/routers/api/v1/user/resetPassword.go new file mode 100644 index 0000000..2c7ecbc --- /dev/null +++ b/routers/api/v1/user/resetPassword.go @@ -0,0 +1,55 @@ +package user + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + + "github.com/kataras/iris/v12" +) + +type resetPasswordStruct struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` +} + +func resetPasswordHandler(ctx iris.Context) { + l.SetFunction("resetPasswordHandler") + + userId, err := ctx.Values().GetInt("userId") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + var resetPassword resetPasswordStruct + if err := ctx.ReadJSON(&resetPassword); err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + user := models.Users{ + ID: uint(userId), + } + result := database.DB.First(&user) + if result.Error != nil { + l.Error("get user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeUserNotExist, iris.StatusInternalServerError) + return + } + + if !user.CheckPassword(resetPassword.OldPassword) { + middleware.Error(ctx, middleware.CodePasswordError, iris.StatusBadRequest) + return + } + + user.Password = user.PasswordHash(resetPassword.NewPassword) + result = database.DB.Save(&user) + if result.Error != nil { + l.Error("save user error: %v", result.Error) + middleware.Error(ctx, middleware.CodeInternalPatchError, iris.StatusInternalServerError) + return + } + + middleware.Success(ctx) +} diff --git a/routers/api/v1/user/routers.go b/routers/api/v1/user/routers.go new file mode 100644 index 0000000..2668309 --- /dev/null +++ b/routers/api/v1/user/routers.go @@ -0,0 +1,32 @@ +package user + +import ( + "megrez/routers/api/v1/middleware" + "megrez/routers/api/v1/user/images" + "megrez/routers/api/v1/user/instances" + "megrez/routers/api/v1/user/servers" + "megrez/services/logger" + + _logger "megrez/libs/logger" + + "github.com/kataras/iris/v12/core/router" +) + +var l *_logger.LoggerStruct + +func InitUser(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.User") + + party.Use(middleware.Auth) + + party.Post("/login", loginHandler) + party.Get("/logout", middleware.AuthCheck, logoutHandler) + party.Post("/register", registerHandler) + party.Get("/profile", middleware.AuthCheck, profileHandler) + party.Post("/resetPassword", middleware.AuthCheck, resetPasswordHandler) + + servers.InitServers(party.Party("/servers")) + instances.InitInstances(party.Party("/instances")) + images.InitImages(party.Party("/images")) +} diff --git a/routers/api/v1/user/servers/detail.go b/routers/api/v1/user/servers/detail.go new file mode 100644 index 0000000..c8059fe --- /dev/null +++ b/routers/api/v1/user/servers/detail.go @@ -0,0 +1,39 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func detailHandler(ctx iris.Context) { + l.SetFunction("detailHandler") + + id, err := ctx.Params().GetUint("id") + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + + server := models.Servers{ + ID: id, + } + result := database.DB.Select("id", "name", "gpu_type", "gpu_num", "gpu_driver_version", "gpu_cuda_version", "cpu_count_per_gpu", "memory_per_gpu", "volume_total", "price", "price_volume", "gpu_used", "volume_used", "created_at").First(&server) + if result.Error != nil { + l.Error("detail server error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerDetailError, iris.StatusInternalServerError) + return + } + + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(id))).Scan(&server.GpuUsed) + server.GpuUsed = server.GpuNum - server.GpuUsed + + redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(id))).Scan(&server.VolumeUsed) + server.VolumeUsed = server.VolumeTotal - server.VolumeUsed + + middleware.Result(ctx, server) +} diff --git a/routers/api/v1/user/servers/list.go b/routers/api/v1/user/servers/list.go new file mode 100644 index 0000000..2957009 --- /dev/null +++ b/routers/api/v1/user/servers/list.go @@ -0,0 +1,62 @@ +package servers + +import ( + "megrez/models" + "megrez/routers/api/v1/middleware" + "megrez/services/database" + "megrez/services/redis" + "strconv" + + "github.com/kataras/iris/v12" +) + +func listHandler(ctx iris.Context) { + l.SetFunction("serversHandler") + var err error + + offset := 0 + limit := 20 + offsetStr := ctx.URLParam("offset") + limitStr := ctx.URLParam("limit") + + if offsetStr != "" { + offset, err = strconv.Atoi(offsetStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + if limitStr != "" { + limit, err = strconv.Atoi(limitStr) + if err != nil { + middleware.Error(ctx, middleware.CodeBadRequest, iris.StatusBadRequest) + return + } + } + + var total int64 + var servers []models.Servers + totalResult := database.DB.Model(&models.Servers{}).Count(&total) + if totalResult.Error != nil { + l.Error("list servers error: %v", totalResult.Error) + middleware.Error(ctx, middleware.CodeAdminServerListError, iris.StatusInternalServerError) + return + } + + result := database.DB.Limit(limit).Offset(offset).Select("id", "name", "gpu_type", "gpu_num", "gpu_driver_version", "gpu_cuda_version", "cpu_count_per_gpu", "memory_per_gpu", "volume_total", "price", "price_volume", "gpu_used", "volume_used", "created_at").Order("id").Find(&servers) + if result.Error != nil { + l.Error("list servers error: %v", result.Error) + middleware.Error(ctx, middleware.CodeAdminServerListError, iris.StatusInternalServerError) + return + } + + for i, server := range servers { + redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID))).Scan(&servers[i].GpuUsed) + servers[i].GpuUsed = server.GpuNum - servers[i].GpuUsed + + redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID))).Scan(&servers[i].VolumeUsed) + servers[i].VolumeUsed = server.VolumeTotal - servers[i].VolumeUsed + } + + middleware.ResultWithTotal(ctx, servers, total) +} diff --git a/routers/api/v1/user/servers/routers.go b/routers/api/v1/user/servers/routers.go new file mode 100644 index 0000000..8c91b06 --- /dev/null +++ b/routers/api/v1/user/servers/routers.go @@ -0,0 +1,19 @@ +package servers + +import ( + "megrez/services/logger" + + "github.com/kataras/iris/v12/core/router" + + _logger "megrez/libs/logger" +) + +var l *_logger.LoggerStruct + +func InitServers(party router.Party) { + l = logger.Logger.Clone() + l.SetModel("Http.API.V1.User.Servers") + + party.Get("/", listHandler) + party.Get("/{id:uint}", detailHandler) +} diff --git a/routers/index/routers.go b/routers/index/routers.go new file mode 100644 index 0000000..0dfa714 --- /dev/null +++ b/routers/index/routers.go @@ -0,0 +1,13 @@ +package index + +import ( + "github.com/kataras/iris/v12" +) + +func InitIndex(app *iris.Application) { + app.HandleDir("/", GetWebFS(), iris.DirOptions{ + IndexName: "/index.html", + Compress: true, + SPA: true, + }) +} diff --git a/routers/index/web.go b/routers/index/web.go new file mode 100644 index 0000000..48fc94b --- /dev/null +++ b/routers/index/web.go @@ -0,0 +1,14 @@ +package index + +import ( + "embed" + "io/fs" +) + +//go:embed web +var assets embed.FS + +func GetWebFS() fs.FS { + serverRoot, _ := fs.Sub(assets, "web") + return serverRoot +} diff --git a/routers/index/web/.gitkeep b/routers/index/web/.gitkeep new file mode 100644 index 0000000..291b675 --- /dev/null +++ b/routers/index/web/.gitkeep @@ -0,0 +1 @@ +Put FrontEnd Dist File Here \ No newline at end of file diff --git a/routers/middleware.go b/routers/middleware.go new file mode 100644 index 0000000..8b7d1ab --- /dev/null +++ b/routers/middleware.go @@ -0,0 +1,11 @@ +package routers + +import ( + "github.com/kataras/iris/v12" +) + +func middleware(ctx iris.Context) { + ctx.Next() + + l.Info("IP: %s - %s %d %s %d - User-Agent: %s", ctx.RemoteAddr(), ctx.Request().Method, ctx.GetStatusCode(), ctx.Request().RequestURI, ctx.Request().ContentLength, ctx.Request().UserAgent()) +} diff --git a/routers/routers.go b/routers/routers.go new file mode 100644 index 0000000..9cd40de --- /dev/null +++ b/routers/routers.go @@ -0,0 +1,21 @@ +package routers + +import ( + _logger "megrez/libs/logger" + v1 "megrez/routers/api/v1" + "megrez/routers/index" + "megrez/services/logger" + + "github.com/kataras/iris/v12" +) + +var l *_logger.LoggerStruct + +func InitRouter(app *iris.Application) { + l = logger.Logger.Clone() + l.SetModel("HTTP") + + app.Use(middleware) + v1.InitApiV1(app.Party("/api/v1")) + index.InitIndex(app) +} diff --git a/services/config/config.go b/services/config/config.go new file mode 100644 index 0000000..eb63ce5 --- /dev/null +++ b/services/config/config.go @@ -0,0 +1,74 @@ +package config + +import "megrez/services/logger" + +var l = logger.Logger.Clone() +var configPath string + +type configStruct struct { + Http httpStruct `yaml:"http,omitempty"` + Database databaseStruct `yaml:"database,omitempty"` + Redis redisStruct `yaml:"redis,omitempty"` + Log logStruct `yaml:"log,omitempty"` + System systemStruct `yaml:"system,omitempty"` +} + +type httpStruct struct { + Host string `yaml:"host,omitempty"` + Port int `yaml:"port,omitempty"` +} + +type databaseStruct struct { + Host string `yaml:"host,omitempty"` + Port int `yaml:"port,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + Database string `yaml:"database,omitempty"` + SSLMode bool `yaml:"ssl,omitempty"` +} + +type redisStruct struct { + Host string `yaml:"host,omitempty"` + Port int `yaml:"port,omitempty"` + Password string `yaml:"password,omitempty"` + Database int `yaml:"database,omitempty"` + SentinelPassword string `yaml:"sentinel_password,omitempty"` +} + +type logStruct struct { + Level string `yaml:"level,omitempty"` + File string `yaml:"file,omitempty"` +} + +type systemStruct struct { + Salt string `yaml:"salt,omitempty"` + Verify bool `yaml:"verify,omitempty"` +} + +var config = configStruct{ + Http: httpStruct{ + Host: "0.0.0.0", + Port: 8080, + }, + Database: databaseStruct{ + Host: "localhost", + Port: 5432, + Username: "GpuManager", + Password: "GpuManager", + Database: "GpuManager", + }, + Redis: redisStruct{ + Host: "localhost", + Port: 6379, + Password: "GpuManager", + Database: 0, + }, + Log: logStruct{ + Level: "DEBUG", + File: "data/logs/backend.log", + }, + System: systemStruct{ + Salt: "", + Verify: false, + }, +} diff --git a/services/config/exporter.go b/services/config/exporter.go new file mode 100644 index 0000000..f25337f --- /dev/null +++ b/services/config/exporter.go @@ -0,0 +1,58 @@ +package config + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +func InitConfig(path string) { + l.SetFunction("InitConfig") + + configPath = path + c, err := os.ReadFile(path) + if err != nil { + l.Fatal("Failed to read config file", err) + } + err = yaml.Unmarshal(c, &config) + if err != nil { + l.Fatal("Failed to unmarshal config file", err) + } + l.SetLevel(config.GetLogLevel()) +} + +func GetDatabase() databaseStruct { + return config.GetDatabase() +} + +func GetHttpAddress() string { + return config.GetHttpAddress() +} + +func GetRedis() redisStruct { + return config.GetRedis() +} + +func GetLogLevel() string { + return config.GetLogLevel() +} + +func GetLogFile() string { + return config.GetLogFile() +} + +func GetSystemSalt() string { + return config.GetSystemSalt() +} + +func SetSystemSalt(salt string) { + config.SetSystemSalt(salt) +} + +func GetSystemVerify() bool { + return config.GetSystemVerify() +} + +func Save() error { + return config.Save() +} diff --git a/services/config/internal.go b/services/config/internal.go new file mode 100644 index 0000000..29fa092 --- /dev/null +++ b/services/config/internal.go @@ -0,0 +1,65 @@ +package config + +import ( + "errors" + "os" + "strconv" + + "gopkg.in/yaml.v3" +) + +func (c *configStruct) GetDatabase() databaseStruct { + return c.Database +} + +func (c *configStruct) GetHttpAddress() string { + return c.Http.Host + ":" + strconv.Itoa(c.Http.Port) +} + +func (c *configStruct) GetRedis() redisStruct { + return c.Redis +} + +func (c *configStruct) GetLogLevel() string { + return c.Log.Level +} + +func (c *configStruct) GetLogFile() string { + return c.Log.File +} + +func (c *configStruct) GetSystemSalt() string { + return c.System.Salt +} + +func (c *configStruct) SetSystemSalt(salt string) { + c.System.Salt = salt +} + +func (c *configStruct) GetSystemVerify() bool { + return c.System.Verify +} + +func (c *configStruct) Save() error { + return c.save() +} + +func (c *configStruct) save() error { + // Save config to file + bytes, err := yaml.Marshal(c) + if err != nil { + return errors.New("yaml marshal Error: " + err.Error()) + } + + f, err := os.OpenFile(configPath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) + if err != nil { + return errors.New("open file Error: " + err.Error()) + } + + _, err = f.Write(bytes) + if err != nil { + return errors.New("write file Error: " + err.Error()) + } + + return nil +} diff --git a/services/database/database.go b/services/database/database.go new file mode 100644 index 0000000..01d7483 --- /dev/null +++ b/services/database/database.go @@ -0,0 +1,39 @@ +package database + +import ( + "fmt" + + "gorm.io/driver/postgres" + "gorm.io/gorm" + + _logger "megrez/libs/logger" + "megrez/services/config" + "megrez/services/logger" + + __logger "gorm.io/gorm/logger" +) + +var l *_logger.LoggerStruct + +var DB *gorm.DB + +func Connect() (err error) { + l = logger.Logger.Clone() + l.SetModel("database") + l.SetFunction("Connect") + + c := config.GetDatabase() + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", c.Host, c.Username, c.Password, c.Database, c.Port) + DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{ + PrepareStmt: true, + Logger: __logger.Default.LogMode(__logger.Silent), + }) + if err != nil { + l.Fatal("Failed to connect to database", err) + } else { + l.Info("Database Connect Success") + } + + autoMigrate() + return +} diff --git a/services/database/migrate.go b/services/database/migrate.go new file mode 100644 index 0000000..337dedf --- /dev/null +++ b/services/database/migrate.go @@ -0,0 +1,34 @@ +package database + +import "megrez/models" + +func autoMigrate() (err error) { + l.SetFunction("autoMigration") + + err = DB.AutoMigrate(&models.Instances{}) + if err != nil { + l.Fatal("Failed to migrate containers", err) + } + + err = DB.AutoMigrate(&models.Orders{}) + if err != nil { + l.Fatal("Failed to migrate orders", err) + } + + err = DB.AutoMigrate(&models.Servers{}) + if err != nil { + l.Fatal("Failed to migrate servers", err) + } + + err = DB.AutoMigrate(&models.System{}) + if err != nil { + l.Fatal("Failed to migrate system", err) + } + + err = DB.AutoMigrate(&models.Users{}) + if err != nil { + l.Fatal("Failed to migrate users", err) + } + + return nil +} diff --git a/services/dispatcher/add.go b/services/dispatcher/add.go new file mode 100644 index 0000000..db69767 --- /dev/null +++ b/services/dispatcher/add.go @@ -0,0 +1,57 @@ +package dispatcher + +import ( + "context" + "megrez/models" + "megrez/services/database" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" +) + +func add(serverID uint, data Data) (err error) { + lc := l.Clone() + lc.SetFunction("add") + + server := models.Servers{ + ID: serverID, + } + result := database.DB.First(&server) + if result.Error != nil { + lc.Error("query server error: %v", result.Error) + return + } + + instance := models.Instances{ + ID: data.InstanceID, + } + result = database.DB.First(&instance) + if result.Error != nil { + lc.Error("query instance error: %v", result.Error) + return result.Error + } + + containerName, volumeName, err := instanceController.Create(&instance) + if err != nil { + lc.Error("create instance error: %v", err) + ctx := context.Background() + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(serverID)), int64(instance.GpuCount)) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(serverID)), int64(instance.VolumeSize+30)) + return + } + + server.GpuUsed += instance.GpuCount + server.VolumeUsed += instance.VolumeSize + 30 + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + + if volumeName != "" { + lc.Info("create volume success: %v", volumeName) + } + lc.Info("create instance success: %v", containerName) + + return +} diff --git a/services/dispatcher/control.go b/services/dispatcher/control.go new file mode 100644 index 0000000..57daad1 --- /dev/null +++ b/services/dispatcher/control.go @@ -0,0 +1,128 @@ +package dispatcher + +import ( + "context" + "errors" + "megrez/models" + "megrez/services/database" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" +) + +func control(serverID uint, data Data) (err error) { + lc := l.Clone() + lc.SetFunction("control") + + server := models.Servers{ + ID: serverID, + } + result := database.DB.First(&server) + if result.Error != nil { + lc.Error("query server error: %v", result.Error) + return + } + + instance := models.Instances{ + ID: data.InstanceID, + } + result = database.DB.First(&instance) + if result.Error != nil { + lc.Error("query instance error: %v", result.Error) + return result.Error + } + + if models.InstanceIngStatusCheck(data.Status) { + lc.Error("instance status error") + return errors.New("instance status error") + } + + if data.Action == instanceController.ActionPause && data.Status != models.InstanceRunning { + lc.Error("instance status error") + return errors.New("instance status error") + } + + if data.Action == instanceController.ActionStart && data.Status != models.InstanceStopped && data.Status != models.InstancePaused { + lc.Error("instance status error") + return errors.New("instance status error") + } + + switch data.Action { + case instanceController.ActionStart: + if data.Status == models.InstancePaused { + err = instanceController.Continue(&instance) + if err != nil { + lc.Error("instance continue error: %v", err) + return + } + } else if data.Status == models.InstanceStopped { + err = instanceController.Restart(&instance) + if err != nil { + lc.Error("instance restart error: %v", err) + ctx := context.Background() + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(serverID)), int64(instance.GpuCount)) + return + } + } + + if data.Status == models.InstanceStopped && instance.Status == models.InstanceRunning { + server.GpuUsed += instance.GpuCount + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + } + + lc.Info("start instance success: %v", instance.ID) + + case instanceController.ActionPause: + err = instanceController.Pause(&instance) + if err != nil { + lc.Error("instance pause error: %v", err) + return + } + + lc.Info("pause instance success: %v", instance.ID) + + case instanceController.ActionStop: + err = instanceController.Stop(&instance) + if err != nil { + lc.Error("instance stop error: %v", err) + return + } + + if (data.Status == models.InstanceRunning || data.Status == models.InstancePaused) && instance.Status == models.InstanceStopped { + server.GpuUsed -= instance.GpuCount + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + } + + lc.Info("stop instance success: %v", instance.ID) + + case instanceController.ActionRestart: + err = instanceController.Restart(&instance) + if err != nil { + lc.Error("instance restart error: %v", err) + return + } + + if data.Status == models.InstanceStopped && instance.Status == models.InstanceRunning { + server.GpuUsed += instance.GpuCount + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + } + + lc.Info("restart instance success: %v", instance.ID) + + default: + } + + return +} diff --git a/services/dispatcher/delete.go b/services/dispatcher/delete.go new file mode 100644 index 0000000..edeafd9 --- /dev/null +++ b/services/dispatcher/delete.go @@ -0,0 +1,58 @@ +package dispatcher + +import ( + "context" + "megrez/models" + "megrez/services/database" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" +) + +func delete(serverID uint, data Data) (err error) { + lc := l.Clone() + lc.SetFunction("delete") + + server := models.Servers{ + ID: serverID, + } + result := database.DB.First(&server) + if result.Error != nil { + lc.Error("query server error: %v", result.Error) + return + } + + instance := models.Instances{ + ID: data.InstanceID, + } + result = database.DB.First(&instance) + if result.Error != nil { + lc.Error("query instance error: %v", result.Error) + return result.Error + } + + err = instanceController.Delete(&instance) + if err != nil { + lc.Error("delete instance error: %v", err) + ctx := context.Background() + if data.Status == models.InstanceRunning || data.Status == models.InstancePaused { + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(serverID)), int64(-instance.GpuCount)) + } + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(serverID)), int64(-instance.VolumeSize-30)) + return + } + + server.VolumeUsed -= instance.VolumeSize + 30 + if data.Status == models.InstanceRunning || data.Status == models.InstancePaused { + server.GpuUsed -= instance.GpuCount + } + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + + lc.Info("delete instance success: %v", instance.ID) + + return +} diff --git a/services/dispatcher/init.go b/services/dispatcher/init.go new file mode 100644 index 0000000..5598f70 --- /dev/null +++ b/services/dispatcher/init.go @@ -0,0 +1,45 @@ +package dispatcher + +import ( + "context" + "megrez/models" + "megrez/services/database" + "megrez/services/logger" + "megrez/services/redis" + "strconv" +) + +func Init() { + l = logger.Logger.Clone() + l.SetModel("dispatcher") + l.SetFunction("Init") + + status = make(map[int]bool) + // run() + + var servers []models.Servers + result := database.DB.Select("id", "gpu_num", "gpu_used", "volume_total", "volume_used").Find(&servers) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return + } + + ctx := context.Background() + + for _, server := range servers { + status[int(server.ID)] = false + go run(server.ID, false) + + _, err := redis.RawDB.Get(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID))).Result() + if err != nil { + l.Info("init server %d remain gpu: %d", server.ID, server.GpuNum-server.GpuUsed) + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(server.ID)), int64(server.GpuNum-server.GpuUsed)) + } + + _, err = redis.RawDB.Get(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID))).Result() + if err != nil { + l.Info("init server %d remain volume: %d", server.ID, server.VolumeTotal-server.VolumeUsed) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(server.ID)), int64(server.VolumeTotal-server.VolumeUsed)) + } + } +} diff --git a/services/dispatcher/interface.go b/services/dispatcher/interface.go new file mode 100644 index 0000000..61805a1 --- /dev/null +++ b/services/dispatcher/interface.go @@ -0,0 +1,33 @@ +package dispatcher + +import ( + _logger "megrez/libs/logger" + "megrez/models" + "megrez/services/instanceController" + "sync" +) + +var l *_logger.LoggerStruct +var status map[int]bool +var mx sync.Mutex + +type Data struct { + Type Type `json:"type"` // 1:Add 2:Contorl 3:Modify 4:Delete + InstanceID uint `json:"instance_id"` + Status models.Status `json:"status"` + + CpuOnly bool `json:"cpu_only,omitempty"` + GpuCount *int `json:"gpu_count,omitempty"` + VolumeSize *int `json:"volume_size,omitempty"` + + Action instanceController.Action `json:"action,omitempty"` +} + +type Type int + +const ( + Add Type = iota + 1 + Control + Modify + Delete +) diff --git a/services/dispatcher/modify.go b/services/dispatcher/modify.go new file mode 100644 index 0000000..67061eb --- /dev/null +++ b/services/dispatcher/modify.go @@ -0,0 +1,84 @@ +package dispatcher + +import ( + "context" + "errors" + "megrez/models" + "megrez/services/database" + "megrez/services/instanceController" + "megrez/services/redis" + "strconv" +) + +type modifyDataStruct struct { + CpuOnly bool `json:"cpu_only"` + GpuCount *int `json:"gpu_count"` + VolumeSize *int `json:"volume_size"` +} + +func modify(serverID uint, data Data) (err error) { + lc := l.Clone() + lc.SetFunction("modify") + + server := models.Servers{ + ID: serverID, + } + result := database.DB.First(&server) + if result.Error != nil { + lc.Error("query server error: %v", result.Error) + return + } + + instance := models.Instances{ + ID: data.InstanceID, + } + result = database.DB.First(&instance) + if result.Error != nil { + lc.Error("query instance error: %v", result.Error) + return result.Error + } + + if data.Status != models.InstanceStopped { + lc.Error("instance status error") + return errors.New("instance status error") + } + + if data.CpuOnly == instance.CpuOnly && data.CpuOnly { + lc.Error("instance already cpu_only mode") + return errors.New("instance already cpu_only mode") + } + + oldVolumeSize := instance.VolumeSize + gpuCount := instance.GpuCount + volumeSize := instance.VolumeSize + if data.GpuCount != nil { + gpuCount = *data.GpuCount + } + if data.VolumeSize != nil { + volumeSize = *data.VolumeSize + } + + err = instanceController.Patch(&instance, gpuCount, volumeSize, data.CpuOnly) + if err != nil { + lc.Error("patch instance error: %v", err) + ctx := context.Background() + redis.RawDB.IncrBy(ctx, "remain_gpu:server:"+strconv.Itoa(int(serverID)), int64(gpuCount)) + redis.RawDB.IncrBy(ctx, "remain_volume:server:"+strconv.Itoa(int(serverID)), int64(volumeSize-oldVolumeSize)) + return + } + + if data.Status == models.InstanceStopped { + server.GpuUsed += instance.GpuCount + } + server.VolumeUsed += instance.VolumeSize - oldVolumeSize + + result = database.DB.Save(&server) + if result.Error != nil { + lc.Error("save server error: %v", result.Error) + return result.Error + } + + lc.Info("modify instance success: %d", instance.ID) + + return +} diff --git a/services/dispatcher/push.go b/services/dispatcher/push.go new file mode 100644 index 0000000..4831cf5 --- /dev/null +++ b/services/dispatcher/push.go @@ -0,0 +1,33 @@ +package dispatcher + +import ( + "context" + "encoding/json" + "megrez/services/redis" + "strconv" +) + +func Push(serverID uint, data Data) (err error) { + l.SetFunction("Push") + + ctx := context.Background() + + dataBytes, err := json.Marshal(data) + if err != nil { + l.Error("Failed to marshal data, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + return + } + + _, err = redis.RawDB.RPush(ctx, "dispatcher:server:"+strconv.Itoa(int(serverID)), string(dataBytes)).Result() + + if err != nil { + l.Error("Failed to push data to redis, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + return + } + + l.Info("Push data to redis success, Server ID: %d, Data: %+v", serverID, data) + + go run(serverID, false) + + return +} diff --git a/services/dispatcher/run.go b/services/dispatcher/run.go new file mode 100644 index 0000000..ed1c2e4 --- /dev/null +++ b/services/dispatcher/run.go @@ -0,0 +1,91 @@ +package dispatcher + +import ( + "context" + "encoding/json" + "megrez/services/redis" + "strconv" +) + +func run(serverID uint, recursion bool) { + lc := l.Clone() + lc.SetFunction("run:" + strconv.Itoa(int(serverID))) + + if !recursion && status[int(serverID)] { + return + } + + mx.Lock() + status[int(serverID)] = true + mx.Unlock() + + ctx := context.Background() + + llen, err := redis.RawDB.LLen(ctx, "dispatcher:server:"+strconv.Itoa(int(serverID))).Result() + if err != nil { + lc.Error("Failed to get length of list, Server ID: %d, Error: %v", serverID, err) + mx.Lock() + status[int(serverID)] = false + mx.Unlock() + return + } + + if llen == 0 { + mx.Lock() + status[int(serverID)] = false + mx.Unlock() + l.Debug("No data in redis, Server ID: %d", serverID) + return + } + + dataStr, err := redis.RawDB.LPop(ctx, "dispatcher:server:"+strconv.Itoa(int(serverID))).Result() + if err != nil { + lc.Error("Failed to pop data from redis, Server ID: %d, Error: %v", serverID, err) + mx.Lock() + status[int(serverID)] = false + mx.Unlock() + + go run(serverID, true) + return + } + + var data Data + err = json.Unmarshal([]byte(dataStr), &data) + if err != nil { + lc.Error("Failed to unmarshal data, Server ID: %d, Data: %s, Error: %v", serverID, dataStr, err) + mx.Lock() + status[int(serverID)] = false + mx.Unlock() + + run(serverID, true) + return + } + + switch data.Type { + case Add: + err = add(serverID, data) + if err != nil { + lc.Error("Failed to add instance, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + } + case Control: + err = control(serverID, data) + if err != nil { + lc.Error("Failed to control instance, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + } + case Modify: + err = modify(serverID, data) + if err != nil { + lc.Error("Failed to modify instance, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + } + case Delete: + err = delete(serverID, data) + if err != nil { + lc.Error("Failed to delete instance, Server ID: %d, Data: %+v, Error: %v", serverID, data, err) + } + default: + } + + go run(serverID, true) + + return +} diff --git a/services/http/http.go b/services/http/http.go new file mode 100644 index 0000000..62004de --- /dev/null +++ b/services/http/http.go @@ -0,0 +1,62 @@ +package http + +import ( + "context" + "fmt" + _logger "megrez/libs/logger" + "megrez/routers" + "megrez/services/config" + "megrez/services/logger" + "time" + + "github.com/kataras/iris/v12" +) + +var l *_logger.LoggerStruct + +func Start() { + l = logger.Logger.Clone() + l.SetModel("HTTP") + l.SetFunction("Start") + + hc := config.GetHttpAddress() + + app := iris.New() + app.Use(iris.Compression) + + idleConnsClosed := make(chan struct{}) + iris.RegisterOnInterrupt(func() { + fmt.Println() + l.SetFunction("CloseHttp") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + err := app.Shutdown(ctx) + if err != nil { + l.Error("Http Shutdown err, Error: %v", err.Error()) + } + l.Info("Http Closed") + + close(idleConnsClosed) + }) + + routers.InitRouter(app) + + l.Info("Http Server Listening on http://%s", hc) + l.Info("Press CTRL+C to shut down.") + err := app.Listen( + hc, + iris.WithConfiguration(iris.Configuration{ + DisableStartupLog: true, + LogLevel: "disable", + Charset: "UTF-8", + EnableOptimizations: true, + }), + iris.WithoutServerError(iris.ErrServerClosed), + ) + if err != nil { + l.Error("Http Server Listen err, Error: %v", err.Error()) + return + } + <-idleConnsClosed +} diff --git a/services/instanceController/chatTable.go b/services/instanceController/chatTable.go new file mode 100644 index 0000000..9f98d1d --- /dev/null +++ b/services/instanceController/chatTable.go @@ -0,0 +1,32 @@ +package instanceController + +const ( + apiPrefix = "/api/v1" +) + +const ( + instancePrefix = "/replicaSet" + instanceRestart = "/restart" + instanceStart = "/continue" + instanceStop = "/stop" + instancePause = "/pause" + instanceHistory = "/history" + instanceRollback = "/rollback" + instanceImage = "/commit" + instanceExecute = "/execute" +) + +const ( + volumePrefix = "/volumes" + volumeSize = "/size" + volumeHistory = "/history" +) + +type Action int + +const ( + ActionStart Action = 1 + ActionPause Action = 2 + ActionStop Action = 3 + ActionRestart Action = 4 +) diff --git a/services/instanceController/continue.go b/services/instanceController/continue.go new file mode 100644 index 0000000..ca812ab --- /dev/null +++ b/services/instanceController/continue.go @@ -0,0 +1,74 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "megrez/models" + "megrez/services/database" + "strconv" +) + +func Continue(instance *models.Instances) (err error) { + l.SetFunction("Start") + + instance.Status = models.InstanceStarting + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + err = continueInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("start instance error: %v", err) + return err + } + + instance.Status = models.InstanceRunning + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + return nil +} + +func continueInstance(ip string, port int, apikey, containerName string) (err error) { + l.SetFunction("startInstance") + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName + instanceStart). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("start instance error: %v", c.GetBody()) + return errors.New("start instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("start instance error: %v", res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/create.go b/services/instanceController/create.go new file mode 100644 index 0000000..131c9eb --- /dev/null +++ b/services/instanceController/create.go @@ -0,0 +1,69 @@ +package instanceController + +import ( + "megrez/models" + "megrez/services/database" +) + +func Create(instance *models.Instances) (containerName, volumeName string, err error) { + l.SetFunction("Create") + + server := models.Servers{ + ID: instance.ServerID, + } + result := database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return "", "", result.Error + } + + if instance.VolumeSize != 0 { + volumeName, err = createVolume(server.IP, server.Port, server.Apikey, instance.VolumeSize) + if err != nil { + l.Error("create volume error: %v", err) + return "", "", err + } + instance.VolumeName = volumeName + } + + containerName, err = createInstance(server.IP, server.Port, server.Apikey, instance.GpuCount, server.CpuCountPerGpu*instance.GpuCount, server.MemoryPerGpu*instance.GpuCount, instance.VolumeName, instance.ImageName) + if err != nil { + l.Error("create instance error: %v", err) + deleteInstance(server.IP, server.Port, server.Apikey, containerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + return "", "", err + } + instance.ContainerName = containerName + + err = SetRootPassword(server.IP, server.Port, server.Apikey, containerName, instance.SshPasswd) + if err != nil { + deleteInstance(server.IP, server.Port, server.Apikey, containerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + l.Error("set root password error: %v", err) + return "", "", err + } + + portBindings, err := GetPortForward(server.IP, server.Port, server.Apikey, containerName) + if err != nil { + deleteInstance(server.IP, server.Port, server.Apikey, containerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + l.Error("get port forward error: %v", err) + return "", "", err + } + + instance.SshAddress = server.IP + ":" + portBindings["22"] + instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] + instance.JupyterAddress = server.IP + ":" + portBindings["8888"] + instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] + + instance.Status = 0 + result = database.DB.Save(&instance) + + return containerName, volumeName, nil +} diff --git a/services/instanceController/createInstance.go b/services/instanceController/createInstance.go new file mode 100644 index 0000000..b993745 --- /dev/null +++ b/services/instanceController/createInstance.go @@ -0,0 +1,93 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/crypto" + "megrez/libs/request" + "strconv" +) + +type createInstanceReqStruct struct { + ImageName string `json:"imageName"` + ReplicaSetName string `json:"replicaSetName"` + CpuCount int `json:"cpuCount"` + GpuCount int `json:"gpuCount"` + Memory string `json:"memory"` + Binds []bindStruct `json:"binds"` + Env []string `json:"env"` + ContainerPorts []string `json:"containerPorts"` +} + +func createInstance(ip string, port int, apikey string, + gpuCount, cpuCount, memorySize int, volumeName string, imageName string, +) (containerName string, err error) { + l.SetFunction("createInstance") + + containerName, err = crypto.GenerateUUIDWithoutHyphen() + if err != nil { + l.Error("generate instance name error: %v", err) + return "", err + } + + data := createInstanceReqStruct{ + ImageName: imageName, + ReplicaSetName: containerName, + CpuCount: cpuCount, + GpuCount: gpuCount, + Memory: strconv.Itoa(memorySize) + "GB", + ContainerPorts: []string{ + "22", // SSH + "6007", // TensorBoard + "8888", // Jupyter Notebook + "3000", // Grafana + "34567", // Custom Port + }, + } + + if volumeName != "" { + data.Binds = []bindStruct{ + { + Src: volumeName, + Dest: "/root/megrez-tmp", + }, + { + Src: "/data/pub", + Dest: "/root/megrez-pub", + }, + } + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return "", err + } + + c := request.NewRequest().Post(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("create instance error: %d", c.GetStatusCode()) + return "", errors.New("create instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return "", err + } + + if res.Code != 200 { + l.Error("create instance code: %d, error: %s", res.Code, res.Msg) + return "", errors.New(res.Msg) + } + + return +} diff --git a/services/instanceController/createVolume.go b/services/instanceController/createVolume.go new file mode 100644 index 0000000..578ddee --- /dev/null +++ b/services/instanceController/createVolume.go @@ -0,0 +1,66 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/crypto" + "megrez/libs/request" + "strconv" +) + +type createVolumeReqStruct struct { + Name string `json:"name"` + Size string `json:"size"` +} + +func createVolume(ip string, port int, apikey string, + volumeSize int, +) (volumeName string, err error) { + l.SetFunction("createVolume") + + volumeName, err = crypto.GenerateUUIDWithoutHyphen() + if err != nil { + l.Error("generate volume name error: %v", err) + return "", err + } + + data := createVolumeReqStruct{ + Name: volumeName, + Size: strconv.Itoa(volumeSize) + "GB", + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return "", err + } + + c := request.NewRequest().Post(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + volumePrefix). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("create volume error code: %d", c.GetStatusCode()) + return "", errors.New("create volume request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return "", err + } + + if res.Code != 200 { + l.Error("create volume code: %d, error: %s", res.Code, res.Msg) + return "", errors.New(res.Msg) + } + + volumeName = volumeName + "-1" + + return +} diff --git a/services/instanceController/delete.go b/services/instanceController/delete.go new file mode 100644 index 0000000..0ba00e1 --- /dev/null +++ b/services/instanceController/delete.go @@ -0,0 +1,47 @@ +package instanceController + +import ( + "megrez/models" + "megrez/services/database" +) + +func Delete(instance *models.Instances) (err error) { + l.SetFunction("Delete") + + instance.Status = models.InstanceDeleting + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + err = deleteInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("delete instance error: %v", err) + return err + } + + if instance.VolumeName != "" { + err = deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + if err != nil { + l.Error("delete volume error: %v", err) + } + } + + result = database.DB.Delete(&instance) + if result.Error != nil { + l.Error("delete instance error: %v", result.Error) + return result.Error + } + + return nil +} diff --git a/services/instanceController/deleteInstance.go b/services/instanceController/deleteInstance.go new file mode 100644 index 0000000..ca56f99 --- /dev/null +++ b/services/instanceController/deleteInstance.go @@ -0,0 +1,39 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" +) + +func deleteInstance(ip string, port int, apikey string, + instanceName string, +) (err error) { + l.SetFunction("deleteInstance") + + c := request.NewRequest().Delete(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + instanceName). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("delete instance error code: %d", c.GetStatusCode()) + return errors.New("delete instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return + } + + if res.Code != 200 { + l.Error("delete instance code: %d, error: %s", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return +} diff --git a/services/instanceController/deleteVolume.go b/services/instanceController/deleteVolume.go new file mode 100644 index 0000000..ae0a343 --- /dev/null +++ b/services/instanceController/deleteVolume.go @@ -0,0 +1,45 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" + "strings" +) + +func deleteVolume(ip string, port int, apikey string, + volumeName string, noAll bool, +) (err error) { + l.SetFunction("deleteVolume") + + url := "http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + volumePrefix + "/" + strings.Split(volumeName, "-")[0] + if noAll { + url = "http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + volumePrefix + "/" + volumeName + "?noall=true" + } + + c := request.NewRequest().Delete(). + SetUrl(url). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("delete volume error code: %d", c.GetStatusCode()) + return errors.New("delete volume request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return + } + + if res.Code != 200 { + l.Error("delete volume code: %d, error: %s", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/execute.go b/services/instanceController/execute.go new file mode 100644 index 0000000..20a7dd9 --- /dev/null +++ b/services/instanceController/execute.go @@ -0,0 +1,58 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" +) + +type executeReq struct { + Cmd []string `json:"cmd"` +} + +func SetRootPassword(ip string, port int, apikey string, + containerName, password string) (err error) { + l.SetFunction("SetRootPassword") + + data := executeReq{ + Cmd: []string{ + "sh", + "-c", + "echo root:" + password + " | chpasswd", + }, + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return err + } + + c := request.NewRequest().Post(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName + instanceExecute). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("set root password error: %d", c.GetStatusCode()) + return errors.New("set root password request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("set root password code: %d, error: %s", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/getPortForward.go b/services/instanceController/getPortForward.go new file mode 100644 index 0000000..7d9df4f --- /dev/null +++ b/services/instanceController/getPortForward.go @@ -0,0 +1,59 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" + "strings" +) + +type infoResStruct struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Info struct { + HostConfig struct { + PortBindings map[string][]struct { + HostIP string `json:"HostIp"` + HostPort string `json:"HostPort"` + } + } `json:"HostConfig"` + } `json:"info"` + } `json:"data"` +} + +func GetPortForward(ip string, port int, apikey string, containerName string) (portBindings map[string]string, err error) { + l.SetFunction("GetPortForward") + + portBindings = make(map[string]string) + + c := request.NewRequest().Get(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("get port forward error: %d", c.GetStatusCode()) + return nil, errors.New("get port forward request error") + } + + var res infoResStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return + } + + if res.Code != 200 { + l.Error("get port forward code: %d, error: %s", res.Code, res.Msg) + return nil, errors.New(res.Msg) + } + + for k, v := range res.Data.Info.HostConfig.PortBindings { + portBindings[strings.Split(k, "/")[0]] = v[0].HostPort + } + + return portBindings, nil +} diff --git a/services/instanceController/history.go b/services/instanceController/history.go new file mode 100644 index 0000000..ee28e49 --- /dev/null +++ b/services/instanceController/history.go @@ -0,0 +1 @@ +package instanceController diff --git a/services/instanceController/init.go b/services/instanceController/init.go new file mode 100644 index 0000000..90ccbf2 --- /dev/null +++ b/services/instanceController/init.go @@ -0,0 +1,25 @@ +package instanceController + +import ( + "megrez/services/logger" + + _logger "megrez/libs/logger" +) + +var l *_logger.LoggerStruct + +func InitInstanceController() { + l = logger.Logger.Clone() + l.SetModel("instanceController") +} + +type bindStruct struct { + Src string `json:"src"` + Dest string `json:"dest"` +} + +type resStruct struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data map[string]any `json:"data"` +} diff --git a/services/instanceController/patch.go b/services/instanceController/patch.go new file mode 100644 index 0000000..b1df6ae --- /dev/null +++ b/services/instanceController/patch.go @@ -0,0 +1,162 @@ +package instanceController + +import ( + "errors" + "megrez/models" + "megrez/services/database" + "strings" +) + +type patchReqStruct struct { + CpuPatch *cpuPatchStruct `json:"cpuPatch"` + GpuPatch *gpuPatchStruct `json:"gpuPatch"` + MemoryPatch *MemoryPatchStruct `json:"memoryPatch"` + VolumePatch *volumePatchStruct `json:"volumePatch"` +} + +type cpuPatchStruct struct { + CpuCount int `json:"cpuCount"` +} + +type gpuPatchStruct struct { + GpuCount int `json:"gpuCount"` +} + +type MemoryPatchStruct struct { + Memory string `json:"memory"` +} + +type volumePatchStruct struct { + OldBind bindStruct `json:"oldBind"` + NewBind bindStruct `json:"newBind"` +} + +func Patch(instance *models.Instances, gpuCount, volumeSize int, cpuOnly bool) (err error) { + l.SetFunction("Patch") + + instance.Status = models.InstanceModifying + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + if gpuCount == instance.GpuCount && volumeSize == instance.VolumeSize && cpuOnly == instance.CpuOnly { + instance.Status = models.InstanceStopped + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + return errors.New("no change") + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + oldVolumeName := instance.VolumeName + if volumeSize != instance.VolumeSize { + newVolumeName, err := patchVolume(server.IP, server.Port, server.Apikey, strings.Split(instance.VolumeName, "-")[0], volumeSize) + if err != nil { + l.Error("patch volume error: %v", err) + return err + } + instance.VolumeName = newVolumeName + instance.VolumeSize = volumeSize + + defer func(server models.Servers, instance *models.Instances, volumeName string) { + err := deleteVolume(server.IP, server.Port, server.Apikey, volumeName, true) + if err != nil { + l.Error("delete volume error: %v", err) + } + }(server, instance, oldVolumeName) + } + + if cpuOnly { + err = patchCpuOnly(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.VolumeName, oldVolumeName) + if err != nil { + l.Error("patch cpu only error: %v", err) + return err + } + + err = SetRootPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) + if err != nil { + deleteInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + l.Error("set root password error: %v", err) + return err + } + + portBindings, err := GetPortForward(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("get port forward error: %v", err) + return err + } + + instance.SshAddress = server.IP + ":" + portBindings["22"] + instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] + instance.JupyterAddress = server.IP + ":" + portBindings["8888"] + instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] + + instance.CpuOnly = true + instance.GpuCount = 0 + instance.Status = models.InstanceRunning + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + return nil + } + + err = patchGpu(server.IP, server.Port, server.Apikey, + instance.ContainerName, + server.CpuCountPerGpu, server.MemoryPerGpu, + instance.VolumeName, oldVolumeName, + gpuCount, instance.GpuCount, + ) + if err != nil { + l.Error("patch gpu error: %v", err) + return err + } + + err = SetRootPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) + if err != nil { + deleteInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + l.Error("set root password error: %v", err) + return err + } + + portBindings, err := GetPortForward(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("get port forward error: %v", err) + return err + } + + instance.SshAddress = server.IP + ":" + portBindings["22"] + instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] + instance.JupyterAddress = server.IP + ":" + portBindings["8888"] + instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] + + instance.CpuOnly = false + instance.GpuCount = gpuCount + instance.Status = models.InstanceRunning + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + return nil +} diff --git a/services/instanceController/patchCpuOnly.go b/services/instanceController/patchCpuOnly.go new file mode 100644 index 0000000..1ca7b44 --- /dev/null +++ b/services/instanceController/patchCpuOnly.go @@ -0,0 +1,73 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" +) + +func patchCpuOnly(ip string, port int, apikey string, + instanceName string, + volumeName string, oldVolumeName string, +) (err error) { + l.SetFunction("patchCpuOnly") + + data := patchReqStruct{ + CpuPatch: &cpuPatchStruct{ + CpuCount: 1, + }, + GpuPatch: &gpuPatchStruct{ + GpuCount: 0, + }, + MemoryPatch: &MemoryPatchStruct{ + Memory: "2GB", + }, + } + + if oldVolumeName != volumeName { + data.VolumePatch = &volumePatchStruct{ + OldBind: bindStruct{ + Src: oldVolumeName, + Dest: "/root/megrez-tmp", + }, + NewBind: bindStruct{ + Src: volumeName, + Dest: "/root/megrez-tmp", + }, + } + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return err + } + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + instanceName). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("patch cpu only error code: %d", c.GetStatusCode()) + return errors.New("patch cpu only request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("patch cpu only code: %d, error: %s", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/patchGpu.go b/services/instanceController/patchGpu.go new file mode 100644 index 0000000..ab71abf --- /dev/null +++ b/services/instanceController/patchGpu.go @@ -0,0 +1,81 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" +) + +func patchGpu(ip string, port int, apikey string, + instanceName string, + cpuCountPerGpu int, memoryPerGpu int, + volumeName string, oldVolumeName string, + gpuCount int, oldGpuCount int, +) (err error) { + l.SetFunction("patchGpu") + + var data patchReqStruct + + if gpuCount == oldGpuCount && volumeName == oldVolumeName { + return nil + } + + if gpuCount != oldGpuCount { + data.GpuPatch = &gpuPatchStruct{ + GpuCount: gpuCount, + } + data.CpuPatch = &cpuPatchStruct{ + CpuCount: cpuCountPerGpu * gpuCount, + } + data.MemoryPatch = &MemoryPatchStruct{ + Memory: strconv.Itoa(memoryPerGpu*gpuCount) + "GB", + } + } + + if volumeName != oldVolumeName { + data.VolumePatch = &volumePatchStruct{ + OldBind: bindStruct{ + Src: oldVolumeName, + Dest: "/root/megrez-tmp", + }, + NewBind: bindStruct{ + Src: volumeName, + Dest: "/root/megrez-tmp", + }, + } + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return err + } + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + instanceName). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("patch gpu error code: %d", c.GetStatusCode()) + return errors.New("patch gpu request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("patch gpu code: %d, error: %s", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/patchVolume.go b/services/instanceController/patchVolume.go new file mode 100644 index 0000000..213479c --- /dev/null +++ b/services/instanceController/patchVolume.go @@ -0,0 +1,64 @@ +package instanceController + +import ( + "bytes" + "encoding/json" + "errors" + "megrez/libs/request" + "strconv" +) + +type patchVolumeReqStruct struct { + Size string `json:"size"` +} + +type patchVolumeResStruct struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Name string `json:"name"` + Size string `json:"size"` + } `json:"data"` +} + +func patchVolume(ip string, port int, apikey string, + volumeName string, newVolumeSize int, +) (newVolumeName string, err error) { + l.SetFunction("patchVolume") + + data := patchVolumeReqStruct{ + Size: strconv.Itoa(newVolumeSize) + "GB", + } + + reqBytes, err := json.Marshal(data) + if err != nil { + l.Error("marshal request data error: %v", err) + return "", err + } + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + volumePrefix + "/" + volumeName + volumeSize). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez"). + SetBody(bytes.NewBuffer(reqBytes)) + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("patch volume error code: %d", c.GetStatusCode()) + return "", errors.New("patch volume request error") + } + + var res patchVolumeResStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return "", err + } + + if res.Code != 200 { + l.Error("patch volume code: %d, error: %s", res.Code, res.Msg) + return "", errors.New(res.Msg) + } + + return res.Data.Name, nil +} diff --git a/services/instanceController/pause.go b/services/instanceController/pause.go new file mode 100644 index 0000000..5bcdeb5 --- /dev/null +++ b/services/instanceController/pause.go @@ -0,0 +1,74 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "megrez/models" + "megrez/services/database" + "strconv" +) + +func Pause(instance *models.Instances) (err error) { + l.SetFunction("Pause") + + instance.Status = models.InstancePausing + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + err = pauseInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("pause instance error: %v", err) + return err + } + + instance.Status = models.InstancePaused + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + return nil +} + +func pauseInstance(ip string, port int, apikey, containerName string) (err error) { + l.SetFunction("pauseInstance") + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName + instancePause). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("pause instance error: %v", c.GetBody()) + return errors.New("pause instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("pause instance error: %v", res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/restart.go b/services/instanceController/restart.go new file mode 100644 index 0000000..f6b29c1 --- /dev/null +++ b/services/instanceController/restart.go @@ -0,0 +1,99 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "megrez/models" + "megrez/services/database" + "strconv" +) + +func Restart(instance *models.Instances) (err error) { + l.SetFunction("Restart") + + if instance.Status == models.InstanceStopped || instance.Status == models.InstancePaused { + instance.Status = models.InstanceStarting + } else { + instance.Status = models.InstanceRestarting + } + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + err = restartInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("restart instance error: %v", err) + return err + } + + err = SetRootPassword(server.IP, server.Port, server.Apikey, instance.ContainerName, instance.SshPasswd) + if err != nil { + deleteInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if instance.VolumeName != "" { + deleteVolume(server.IP, server.Port, server.Apikey, instance.VolumeName, false) + } + l.Error("set root password error: %v", err) + return err + } + + portBindings, err := GetPortForward(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("get port forward error: %v", err) + return err + } + + instance.SshAddress = server.IP + ":" + portBindings["22"] + instance.TensorBoardAddress = server.IP + ":" + portBindings["6007"] + instance.JupyterAddress = server.IP + ":" + portBindings["8888"] + instance.GrafanaAddress = server.IP + ":" + portBindings["3000"] + + instance.Status = models.InstanceRunning + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + return nil +} + +func restartInstance(ip string, port int, apikey, containerName string) (err error) { + l.SetFunction("restartInstance") + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName + instanceRestart). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("restart instance error: %v", c.GetBody()) + return errors.New("restart instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("restart instance error: %v", res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/instanceController/rollback.go b/services/instanceController/rollback.go new file mode 100644 index 0000000..ee28e49 --- /dev/null +++ b/services/instanceController/rollback.go @@ -0,0 +1 @@ +package instanceController diff --git a/services/instanceController/stop.go b/services/instanceController/stop.go new file mode 100644 index 0000000..be965cf --- /dev/null +++ b/services/instanceController/stop.go @@ -0,0 +1,74 @@ +package instanceController + +import ( + "encoding/json" + "errors" + "megrez/libs/request" + "megrez/models" + "megrez/services/database" + "strconv" +) + +func Stop(instance *models.Instances) (err error) { + l.SetFunction("Stop") + + instance.Status = models.InstanceStopping + result := database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + server := models.Servers{ + ID: instance.ServerID, + } + result = database.DB.First(&server) + if result.Error != nil { + l.Error("query server error: %v", result.Error) + return result.Error + } + + err = stopInstance(server.IP, server.Port, server.Apikey, instance.ContainerName) + if err != nil { + l.Error("stop instance error: %v", err) + return err + } + + instance.Status = models.InstanceStopped + result = database.DB.Save(&instance) + if result.Error != nil { + l.Error("save instance error: %v", result.Error) + return result.Error + } + + return nil +} + +func stopInstance(ip string, port int, apikey, containerName string) (err error) { + l.SetFunction("stopInstance") + + c := request.NewRequest().Patch(). + SetUrl("http://" + ip + ":" + strconv.Itoa(port) + apiPrefix + instancePrefix + "/" + containerName + instanceStop). + SetAuthorization("Bearer " + apikey). + SetUserAgent("megrez") + c.Do() + + if c.GetStatusCode() != 200 { + l.Error("stop instance error: %v", c.GetBody()) + return errors.New("stop instance request error") + } + + var res resStruct + err = json.Unmarshal(c.GetBody(), &res) + if err != nil { + l.Error("unmarshal response data error: %v", err) + return err + } + + if res.Code != 200 { + l.Error("stop instance code: %d, error: %v", res.Code, res.Msg) + return errors.New(res.Msg) + } + + return nil +} diff --git a/services/logger/logger.go b/services/logger/logger.go new file mode 100644 index 0000000..4cf7ec7 --- /dev/null +++ b/services/logger/logger.go @@ -0,0 +1,13 @@ +package logger + +import "megrez/libs/logger" + +var Logger *logger.LoggerStruct + +func init() { + Logger, _ = logger.NewLogger(logger.DEBUG, "stdout") +} + +func InitLogger(level, path string) { + Logger, _ = logger.NewLogger(level, path) +} diff --git a/services/redis/redis.go b/services/redis/redis.go new file mode 100644 index 0000000..8f7d816 --- /dev/null +++ b/services/redis/redis.go @@ -0,0 +1,61 @@ +package redis + +import ( + "strconv" + "time" + + "github.com/kataras/iris/v12/sessions/sessiondb/redis" + goRedis "github.com/redis/go-redis/v9" + + _logger "megrez/libs/logger" + "megrez/services/config" + "megrez/services/logger" +) + +var l *_logger.LoggerStruct +var DB *redis.Database +var RawDB *goRedis.Client + +func Connect() (err error) { + l = logger.Logger.Clone() + l.SetModel("redis") + l.SetFunction("Connect") + + rd := config.GetRedis() + DB = redis.New(redis.Config{ + Network: "tcp", + Addr: rd.Host + ":" + strconv.Itoa(rd.Port), + Timeout: time.Duration(30) * time.Second, + MaxActive: 10, + Password: rd.Password, + Database: strconv.Itoa(rd.Database), + Prefix: "", + }) + + RawDB = goRedis.NewClient(&goRedis.Options{ + Addr: rd.Host + ":" + strconv.Itoa(rd.Port), + Password: rd.Password, + DB: rd.Database, + }) + + l.Info("Redis connected") + + return nil +} + +func Close() (err error) { + l.SetFunction("Close") + + if err = DB.Close(); err != nil { + l.Error("Failed to close Redis Client, Error: %v", err) + return + } + + if err = RawDB.Close(); err != nil { + l.Error("Failed to close Redis Client, Error: %v", err) + return + } + + l.Info("Redis closed") + return +} diff --git a/services/system/check.go b/services/system/check.go new file mode 100644 index 0000000..889b7e0 --- /dev/null +++ b/services/system/check.go @@ -0,0 +1,25 @@ +package system + +import ( + _logger "megrez/libs/logger" + "megrez/services/config" + "megrez/services/logger" +) + +var l *_logger.LoggerStruct + +func Check() (err error) { + l = logger.Logger.Clone() + l.SetModel("system") + l.SetFunction("Check") + + salt := config.GetSystemSalt() + if salt == "" { + l.Info("System not init, initializing") + systemInit() + } else { + l.Debug("System already init") + } + + return nil +} diff --git a/services/system/init.go b/services/system/init.go new file mode 100644 index 0000000..9f8dde8 --- /dev/null +++ b/services/system/init.go @@ -0,0 +1,36 @@ +package system + +import ( + "megrez/libs/crypto" + "megrez/models" + "megrez/services/config" + "megrez/services/database" +) + +func systemInit() (err error) { + l.SetFunction("systemInit") + + salt := crypto.Hex(32) + config.SetSystemSalt(salt) + err = config.Save() + if err != nil { + l.Error("Save config failed, Error: %v", err) + return + } + + user := models.Users{ + Username: "admin", + Email: "admin@gpuManager.com", + Role: 3, + } + user.Password = user.PasswordHash("admin123456") + result := database.DB.Create(&user) + if result.Error != nil { + l.Error("Create admin user failed, Error: %v", result.Error) + return + } + + l.Info("System init success") + + return +}