mirror of
https://github.com/rustfs/console.git
synced 2026-09-18 09:10:17 +08:00
Merge pull request #2 from rustfs/base-route
feat: improve TypeScript type safety and code quality
This commit is contained in:
@@ -1,7 +1,22 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mkdir:*)"
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(rg:*)",
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(pkill:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(rm:*)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(npm run type-check:*)",
|
||||
"Bash(npm run lint)",
|
||||
"Bash(mv:*)",
|
||||
"Bash(cp:*)",
|
||||
"Bash(npm run:*)",
|
||||
"Bash(git add:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{json,yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -6,6 +6,9 @@ APP_DESCRIPTION="RustFS is a distributed file system written in Rust."
|
||||
# Admin API base URL
|
||||
API_BASE_URL=http://localhost:9000/rustfs/admin/v3
|
||||
|
||||
# Console base URL
|
||||
BASE_URL=/rustfs/console/
|
||||
|
||||
# S3 API base URL
|
||||
S3_ENDPOINT=http://localhost:9000
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
.nuxt
|
||||
.output
|
||||
.git
|
||||
*.log
|
||||
public
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 100,
|
||||
"endOfLine": "lf",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
||||
@@ -1,21 +1,201 @@
|
||||
MIT License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2024 rustfs
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
1. Definitions.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2024 Beijing Henghesha Technology Co., Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -1,75 +1,349 @@
|
||||
# RustFS Console
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
[](LICENSE)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://vuejs.org/)
|
||||
[](https://nuxt.com/)
|
||||
|
||||
## Setup
|
||||
A modern, responsive web management console for RustFS distributed file system, built with Vue 3, Nuxt 3, and TypeScript.
|
||||
|
||||
Make sure to install dependencies:
|
||||
## ✨ Features
|
||||
|
||||
- 🚀 **Modern Stack**: Built with Vue 3, Nuxt 3, TypeScript, and Tailwind CSS
|
||||
- 🎨 **Beautiful UI**: Clean and intuitive interface with dark mode support
|
||||
- 🌍 **Internationalization**: Full i18n support with English and Chinese
|
||||
- 📱 **Responsive Design**: Mobile-friendly design that works on all devices
|
||||
- ⚡ **High Performance**: Optimized with caching, lazy loading, and code splitting
|
||||
- 🔒 **Enterprise Security**: Secure authentication and authorization
|
||||
- 📊 **Rich Analytics**: Comprehensive monitoring and metrics dashboard
|
||||
- 🛠 **Developer Experience**: Hot reload, TypeScript, ESLint, and Prettier
|
||||
|
||||
## 🎯 Core Functionalities
|
||||
|
||||
### Storage Management
|
||||
- **Bucket Operations**: Create, delete, configure buckets
|
||||
- **Object Management**: Upload, download, delete objects with batch operations
|
||||
- **File Browser**: Intuitive file explorer with search and filtering
|
||||
- **Access Control**: Granular permissions and bucket policies
|
||||
|
||||
### User & Access Management
|
||||
- **User Management**: Create and manage users and groups
|
||||
- **Access Keys**: Generate and manage API credentials
|
||||
- **Policies**: Fine-grained access control policies
|
||||
- **Authentication**: Multiple authentication methods (AccessKey, STS)
|
||||
|
||||
### System Monitoring
|
||||
- **Performance Metrics**: Real-time system performance monitoring
|
||||
- **Usage Analytics**: Storage usage and bandwidth statistics
|
||||
- **Health Monitoring**: System health and status indicators
|
||||
- **Audit Logs**: Comprehensive audit trail and logging
|
||||
|
||||
### Advanced Features
|
||||
- **Lifecycle Management**: Automated data lifecycle policies
|
||||
- **Replication**: Cross-region and site replication
|
||||
- **Tiering**: Intelligent data tiering and archival
|
||||
- **Encryption**: Data encryption at rest and in transit
|
||||
- **Event Notifications**: Real-time event notifications
|
||||
|
||||
## 🛠 Technical Stack
|
||||
|
||||
- **Frontend Framework**: [Vue 3](https://vuejs.org/) with Composition API
|
||||
- **Meta Framework**: [Nuxt 3](https://nuxt.com/) for SSR/SPA
|
||||
- **Language**: [TypeScript](https://www.typescriptlang.org/) for type safety
|
||||
- **Styling**: [Tailwind CSS](https://tailwindcss.com/) + [Naive UI](https://www.naiveui.com/)
|
||||
- **State Management**: [Pinia](https://pinia.vuejs.org/) for reactive state
|
||||
- **HTTP Client**: Custom AWS-compatible client with automatic signing
|
||||
- **Build Tool**: [Vite](https://vitejs.dev/) for fast development and building
|
||||
- **Package Manager**: npm/pnpm/yarn
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- npm/pnpm/yarn package manager
|
||||
- RustFS backend server running
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# npm
|
||||
# Clone the repository
|
||||
git clone https://github.com/your-org/rustfs-console.git
|
||||
cd rustfs-console
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
# or
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
# or
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# npm
|
||||
# Start development server
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
# or
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
# or
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
The development server will start at `http://localhost:3000`.
|
||||
|
||||
Build the application for production:
|
||||
### Environment Configuration
|
||||
|
||||
Create a `.env` file in the root directory:
|
||||
|
||||
```env
|
||||
# Application Configuration
|
||||
APP_NAME=RustFS
|
||||
APP_DESCRIPTION=RustFS Console
|
||||
BASE_URL=/rustfs/console/
|
||||
|
||||
# API Configuration
|
||||
API_BASE_URL=http://localhost:9000/rustfs/admin/v3
|
||||
SERVER_HOST=http://localhost:9000
|
||||
|
||||
# S3 Configuration
|
||||
S3_REGION=us-east-1
|
||||
S3_ENDPOINT=http://localhost:9000
|
||||
|
||||
# Session Configuration
|
||||
SESSION_DURATION_SECONDS=43200
|
||||
```
|
||||
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
# npm
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
# or
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
# or
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
# Preview production build locally
|
||||
npm run preview
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
## 🔧 Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
├── assets/ # Static assets (images, styles)
|
||||
├── components/ # Vue components
|
||||
│ ├── access-keys/ # Access key management
|
||||
│ ├── buckets/ # Bucket operations
|
||||
│ ├── object/ # Object management
|
||||
│ ├── users/ # User management
|
||||
│ └── ui/ # Reusable UI components
|
||||
├── composables/ # Vue composables
|
||||
├── layouts/ # Nuxt layouts
|
||||
├── middleware/ # Route middleware
|
||||
├── pages/ # File-based routing
|
||||
├── plugins/ # Nuxt plugins
|
||||
├── server/ # Server-side code
|
||||
├── store/ # Pinia stores
|
||||
├── types/ # TypeScript definitions
|
||||
├── utils/ # Utility functions
|
||||
└── nuxt.config.ts # Nuxt configuration
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
We maintain high code quality standards with:
|
||||
|
||||
- **TypeScript**: Full type safety and better developer experience
|
||||
- **ESLint**: Code linting and style enforcement
|
||||
- **Prettier**: Automatic code formatting
|
||||
- **Vue TSC**: Vue-specific TypeScript checking
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
# Run type checking
|
||||
npm run type-check
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
# Run linting
|
||||
npm run lint
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
# Fix linting issues
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
### Key Architecture Patterns
|
||||
|
||||
- **Composables**: Reusable logic with Vue Composition API
|
||||
- **Plugin System**: Modular configuration and service injection
|
||||
- **Error Boundaries**: Comprehensive error handling and user feedback
|
||||
- **Performance Optimization**: Smart caching, lazy loading, and code splitting
|
||||
- **Security**: Input validation, XSS protection, and secure authentication
|
||||
|
||||
## 🎨 Customization
|
||||
|
||||
### Theming
|
||||
|
||||
The application supports light and dark themes. Customize themes in:
|
||||
- `assets/css/tailwind.css` - Tailwind configuration
|
||||
- `tailwind.config.ts` - Theme colors and design tokens
|
||||
- `components/theme-switcher.vue` - Theme switching logic
|
||||
|
||||
### Internationalization
|
||||
|
||||
Add new languages by:
|
||||
1. Creating locale files in `i18n/locales/`
|
||||
2. Updating `nuxt.config.ts` locales configuration
|
||||
3. Adding language switcher options
|
||||
|
||||
### Component Library
|
||||
|
||||
Built on Naive UI with custom components in `components/ui/`. Extend the design system by:
|
||||
- Adding new components to `components/ui/`
|
||||
- Following established naming conventions
|
||||
- Including proper TypeScript definitions
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
```dockerfile
|
||||
FROM node:18-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:18-alpine AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/.output ./.output
|
||||
EXPOSE 3000
|
||||
CMD ["node", ".output/server/index.mjs"]
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Production deployment requires these environment variables:
|
||||
|
||||
- `API_BASE_URL`: RustFS backend API endpoint
|
||||
- `SERVER_HOST`: RustFS server host
|
||||
- `BASE_URL`: Application base path
|
||||
- `NODE_ENV`: Set to `production`
|
||||
|
||||
### Build Optimization
|
||||
|
||||
For optimal production builds:
|
||||
- Enable gzip/brotli compression
|
||||
- Configure proper caching headers
|
||||
- Use CDN for static assets
|
||||
- Monitor bundle size and performance
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please follow our contribution guidelines:
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. **Fork** the repository
|
||||
2. **Clone** your fork locally
|
||||
3. **Create** a feature branch: `git checkout -b feature/amazing-feature`
|
||||
4. **Install** dependencies: `npm install`
|
||||
5. **Make** your changes following our coding standards
|
||||
6. **Test** your changes: `npm run type-check && npm run lint`
|
||||
7. **Commit** your changes: `git commit -m 'feat: add amazing feature'`
|
||||
8. **Push** to your branch: `git push origin feature/amazing-feature`
|
||||
9. **Submit** a Pull Request
|
||||
|
||||
### Coding Standards
|
||||
|
||||
- **TypeScript**: Use TypeScript for all new code
|
||||
- **Vue 3**: Use Composition API and `<script setup>` syntax
|
||||
- **Naming**: Use kebab-case for files and components
|
||||
- **Comments**: Add JSDoc comments for public APIs
|
||||
- **Testing**: Include tests for new features
|
||||
- **Performance**: Consider performance implications
|
||||
|
||||
### Code Style
|
||||
|
||||
We use automated code formatting:
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
npm run lint:fix
|
||||
|
||||
# Check formatting
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Commit Convention
|
||||
|
||||
Follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
- `feat`: New features
|
||||
- `fix`: Bug fixes
|
||||
- `docs`: Documentation changes
|
||||
- `style`: Code style changes
|
||||
- `refactor`: Code refactoring
|
||||
- `test`: Test additions/modifications
|
||||
- `chore`: Build process or auxiliary tool changes
|
||||
|
||||
### Pull Request Guidelines
|
||||
|
||||
- **Title**: Use clear, descriptive titles
|
||||
- **Description**: Explain what changes were made and why
|
||||
- **Testing**: Describe how the changes were tested
|
||||
- **Breaking Changes**: Clearly mark any breaking changes
|
||||
- **Screenshots**: Include screenshots for UI changes
|
||||
|
||||
### Issue Reporting
|
||||
|
||||
When reporting issues:
|
||||
|
||||
1. **Search** existing issues first
|
||||
2. **Use** the issue templates
|
||||
3. **Provide** detailed reproduction steps
|
||||
4. **Include** system information and error logs
|
||||
5. **Add** relevant labels
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
# Clone and setup
|
||||
git clone https://github.com/your-username/rustfs-console.git
|
||||
cd rustfs-console
|
||||
npm install
|
||||
|
||||
# Create feature branch
|
||||
git checkout -b feature/your-feature
|
||||
|
||||
# Start development
|
||||
npm run dev
|
||||
|
||||
# Run checks before committing
|
||||
npm run type-check
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- [Vue.js](https://vuejs.org/) - The progressive JavaScript framework
|
||||
- [Nuxt](https://nuxt.com/) - The intuitive Vue framework
|
||||
- [Naive UI](https://www.naiveui.com/) - A Vue 3 component library
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - A utility-first CSS framework
|
||||
- [Iconify](https://iconify.design/) - Universal icon framework
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- **Documentation**: [RustFS Docs](https://docs.rustfs.com)
|
||||
- **Community**: [GitHub Discussions](https://github.com/your-org/rustfs-console/discussions)
|
||||
- **Issues**: [GitHub Issues](https://github.com/your-org/rustfs-console/issues)
|
||||
- **Email**: support@rustfs.com
|
||||
|
||||
---
|
||||
|
||||
Made with ❤️ by the RustFS team
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import navs from '~/config/navs'
|
||||
import type { AppConfig } from '~/types/app-config'
|
||||
|
||||
export default defineAppConfig({
|
||||
export default defineAppConfig<AppConfig>({
|
||||
navs: navs,
|
||||
name: process.env.APP_NAME || 'RustFS',
|
||||
description: process.env.APP_DESCRIPTION || 'RustFS is a distributed file system written in Rust.',
|
||||
|
||||
@@ -288,7 +288,7 @@ const getObjectLockConfig = async () => {
|
||||
/**********object lock ***********************/
|
||||
|
||||
/******** policy ***********************/
|
||||
import { setBucketPolicy, getBucketPolicy as getBucketPolicyFn } from "~/utils/bucketPolicy";
|
||||
import { setBucketPolicy, getBucketPolicy as getBucketPolicyFn } from "~/utils/bucket-policy";
|
||||
|
||||
const bucketPolicy = ref("public");
|
||||
const getbucketPolicy = async () => {
|
||||
|
||||
@@ -125,7 +125,18 @@ import {
|
||||
NInputNumber,
|
||||
NSelect,
|
||||
} from "naive-ui";
|
||||
import { randomUUID } from "uncrypto";
|
||||
// 使用 Web Crypto API 或 Node.js crypto 模块
|
||||
const randomUUID = () => {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
// 兜底方案
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { putBucketLifecycleConfiguration, getBucketVersioning, getBucketLifecycleConfiguration } = useBucket({});
|
||||
|
||||
+28
-44
@@ -3,32 +3,39 @@ import { useI18n } from "vue-i18n";
|
||||
import { RouterLink } from "vue-router";
|
||||
import { useSidebarStore } from "~/store/sidebar";
|
||||
import type { SiteConfig } from "~/types/config";
|
||||
import type { AppConfig, NavItem } from "~/types/app-config";
|
||||
|
||||
const { t } = useI18n();
|
||||
const appConfig = useAppConfig();
|
||||
const siteConfig = useNuxtApp().$siteConfig as unknown as SiteConfig;
|
||||
const appConfig = useAppConfig() as unknown as AppConfig;
|
||||
const route = useRoute();
|
||||
const sidebarStore = useSidebarStore();
|
||||
const isCollapsed = computed(() => sidebarStore.isCollapsed);
|
||||
|
||||
// 安全地获取 siteConfig,如果失败则使用默认值
|
||||
let siteConfig: SiteConfig;
|
||||
try {
|
||||
siteConfig = useNuxtApp().$siteConfig as SiteConfig;
|
||||
} catch (error) {
|
||||
console.warn('Failed to load siteConfig, using defaults:', error);
|
||||
siteConfig = {
|
||||
serverHost: window.location.origin,
|
||||
api: { baseURL: `${window.location.origin}/rustfs/admin/v3` },
|
||||
s3: {
|
||||
endpoint: window.location.origin,
|
||||
region: 'us-east-1',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
sidebarStore.toggleSidebar();
|
||||
};
|
||||
|
||||
const options = computed(() => {
|
||||
return appConfig.navs.map(
|
||||
(nav: {
|
||||
label: string;
|
||||
to?: string;
|
||||
type?: string;
|
||||
icon?: string;
|
||||
target?: string;
|
||||
children?: {
|
||||
label: string;
|
||||
to: string;
|
||||
icon?: string;
|
||||
}[];
|
||||
}) => {
|
||||
// 缓存导航配置的生成逻辑,避免每次重新计算
|
||||
const navOptions = computed(() => {
|
||||
return appConfig.navs.map((nav: NavItem) => {
|
||||
let item: {
|
||||
key: string;
|
||||
type?: string;
|
||||
@@ -59,7 +66,7 @@ const options = computed(() => {
|
||||
return {
|
||||
key: child.label,
|
||||
type: "item",
|
||||
label: () => h(RouterLink, { to: child.to }, { default: () => t(child.label) }),
|
||||
label: () => h(RouterLink, { to: child.to || '/' }, { default: () => t(child.label) }),
|
||||
icon: child.icon ? iconRender(child.icon) : undefined,
|
||||
};
|
||||
});
|
||||
@@ -69,6 +76,9 @@ const options = computed(() => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// 直接使用计算属性,不需要 readonly 包装
|
||||
const menuOptions = navOptions;
|
||||
</script>
|
||||
<template>
|
||||
<n-layout-sider
|
||||
@@ -90,17 +100,6 @@ const options = computed(() => {
|
||||
</n-avatar>
|
||||
<h2 v-else class="text-center text-2xl flex">
|
||||
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
|
||||
<span
|
||||
v-if="siteConfig.license.name && siteConfig.license.expired != 0"
|
||||
class="flex items-center justify-center text-[9px] leading-[9px] ml-1 p-1 bg-orange-600 text-white rounded rounded-bl-none">
|
||||
{{ t("PRO") }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="flex items-center justify-center text-[9px] leading-[9px] ml-1 p-1 bg-green-600 text-white rounded rounded-bl-none">
|
||||
{{ t("OSS") }}
|
||||
</span>
|
||||
|
||||
<span class="sr-only">{{ appConfig.name }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -115,7 +114,7 @@ const options = computed(() => {
|
||||
:root-indent="12"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="options"
|
||||
:options="menuOptions"
|
||||
default-expand-all
|
||||
class="flex-1" />
|
||||
</div>
|
||||
@@ -124,21 +123,6 @@ const options = computed(() => {
|
||||
<Icon name="ri:menu-unfold-fill" class="cursor-pointer text-xl" @click="toggleSidebar" :title="t('Expand')" />
|
||||
</div>
|
||||
|
||||
<div v-if="!isCollapsed" class="flex flex-col p-4 text-gray-500">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="ri-server-line" />
|
||||
<span>{{ t("Version") }}: {{ siteConfig.release.version }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="ri-calendar-line" />
|
||||
<span>{{ t("Build Date") }}:{{ siteConfig.release.date }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="ri-verified-badge-line" />
|
||||
<span>{{ t("License") }}:{{ siteConfig.license.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t dark:border-neutral-800 p-2" :class="{ 'flex justify-between items-center': !isCollapsed }">
|
||||
<!-- 语言切换组件 -->
|
||||
<div class="px-2 py-2">
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { download } from '@/utils/exportFile'
|
||||
import { download } from '@/utils/export-file'
|
||||
const visible = ref(false)
|
||||
|
||||
const accessKey = ref('')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const useAccessKeys = () => {
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const listUserServiceAccounts = async (params: object) => {
|
||||
const listUserServiceAccounts = async (params: Record<string, string> = {}) => {
|
||||
return await $api.get("/list-service-accounts", { params })
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export function useAuth() {
|
||||
}
|
||||
|
||||
const getCredentials = () => {
|
||||
if (!isValiedCredentials(store.value)) {
|
||||
if (!isValidCredentials(store.value)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function useAuth() {
|
||||
|
||||
const isExpired = (expiration: string) => expiration ? new Date(expiration) < new Date() : false
|
||||
|
||||
const isValiedCredentials = (credentials: Credentials) => {
|
||||
const isValidCredentials = (credentials: Credentials) => {
|
||||
return !!credentials?.AccessKeyId && !!credentials?.SecretAccessKey && !!credentials?.SessionToken && credentials?.Expiration && !isExpired(credentials.Expiration)
|
||||
}
|
||||
|
||||
@@ -58,6 +58,6 @@ export function useAuth() {
|
||||
logout,
|
||||
logoutAndRedirect,
|
||||
credentials: ref<Credentials | undefined>(getCredentials()),
|
||||
isAuthenticated: ref(isValiedCredentials(store.value)),
|
||||
isAuthenticated: ref(isValidCredentials(store.value)),
|
||||
}
|
||||
}
|
||||
|
||||
+4
-8
@@ -1,4 +1,6 @@
|
||||
// icons: https://icones.js.org/collection/ri
|
||||
import type { NavItem } from '~/types/app-config'
|
||||
|
||||
export default [
|
||||
{
|
||||
label: "Browser",
|
||||
@@ -73,15 +75,9 @@ export default [
|
||||
key: "divider-2",
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
to: "/settings",
|
||||
icon: "ri:settings-3-line",
|
||||
},
|
||||
{
|
||||
label: "License",
|
||||
// to: 'https://github.com/rustfs/rustfs?tab=Apache-2.0-1-ov-file#readme',
|
||||
to: "/licenseinfo",
|
||||
to: "/license",
|
||||
icon: "ri:copyright-line",
|
||||
},
|
||||
{
|
||||
@@ -90,4 +86,4 @@ export default [
|
||||
target: "_blank",
|
||||
icon: "ri:file-list-3-line",
|
||||
},
|
||||
];
|
||||
] as NavItem[];
|
||||
|
||||
+34
-17
@@ -133,20 +133,9 @@
|
||||
"Welcome Back": "Welcome Back",
|
||||
"Login Problems": "Login Problems",
|
||||
"Bucket Not Empty": "Bucket Not Empty",
|
||||
"Server Configuration": "Server Configuration",
|
||||
"Server URL": "Server URL",
|
||||
"Enter complete server URL (e.g., http://localhost:9000)": "Enter complete server URL (e.g., http://localhost:9000)",
|
||||
"Server URL is required": "Server URL is required",
|
||||
"Only HTTP and HTTPS protocols are supported": "Only HTTP and HTTPS protocols are supported",
|
||||
"Invalid hostname": "Invalid hostname",
|
||||
"Invalid URL format": "Invalid URL format",
|
||||
"Protocol": "Protocol",
|
||||
"Host": "Host",
|
||||
"Port": "Port",
|
||||
"Server configuration saved": "Server configuration saved",
|
||||
"Save Configuration": "Save Configuration",
|
||||
"Clear Configuration": "Clear Configuration",
|
||||
"Generated Configuration": "Generated Configuration",
|
||||
"Current Configuration": "Current Configuration",
|
||||
"Information": "Information",
|
||||
"The system automatically uses the current host as the API base URL. No manual configuration is required.": "The system automatically uses the current host as the API base URL. No manual configuration is required.",
|
||||
"Upload File": "Upload File",
|
||||
"Folder": "Folder",
|
||||
"Update Time": "Update Time",
|
||||
@@ -371,7 +360,35 @@
|
||||
"Please enter bandwidth": "Please enter bandwidth",
|
||||
"Please enter storage type": "Please enter storage type",
|
||||
"Add Replication Rule": "Add Replication Rule",
|
||||
"No valid server configuration detected, please check server configuration": "No valid server configuration detected, please check server configuration",
|
||||
"Login failed, please check server configuration": "Login failed, please check server configuration",
|
||||
"Click to modify": "Click to modify"
|
||||
"Click to modify": "Click to modify",
|
||||
|
||||
"Server Configuration": "Server Configuration",
|
||||
"Please configure your RustFS server address": "Please configure your RustFS server address",
|
||||
"Server Address": "Server Address",
|
||||
"Please enter server address (e.g., http://localhost:9000)": "Please enter server address (e.g., http://localhost:9000)",
|
||||
"Example: http://localhost:9000 or https://your-domain.com": "Example: http://localhost:9000 or https://your-domain.com",
|
||||
"Save Configuration": "Save Configuration",
|
||||
"Skip": "Skip",
|
||||
"Need help?": "Need help?",
|
||||
"View Documentation": "View Documentation",
|
||||
"Need to configure server?": "Need to configure server?",
|
||||
"Please enter server address": "Please enter server address",
|
||||
"Server configuration saved successfully": "Server configuration saved successfully",
|
||||
"Invalid server address format": "Invalid server address format",
|
||||
"Settings": "Settings",
|
||||
"Current Configuration": "Current Configuration",
|
||||
"Not configured": "Not configured",
|
||||
"API Base URL": "API Base URL",
|
||||
"S3 Endpoint": "S3 Endpoint",
|
||||
"S3 Region": "S3 Region",
|
||||
"Reset to Default": "Reset to Default",
|
||||
"Configuration Information": "Configuration Information",
|
||||
"Configuration is saved locally in your browser": "Configuration is saved locally in your browser",
|
||||
"Page will refresh automatically after saving configuration": "Page will refresh automatically after saving configuration",
|
||||
"Make sure the server address is accessible from your network": "Make sure the server address is accessible from your network",
|
||||
"Failed to load current configuration": "Failed to load current configuration",
|
||||
"Configuration saved successfully": "Configuration saved successfully",
|
||||
"Configuration reset successfully": "Configuration reset successfully",
|
||||
"Reset to default successfully": "Reset to default successfully",
|
||||
"Leave empty to use current host as default": "Leave empty to use current host as default"
|
||||
}
|
||||
|
||||
+35
-17
@@ -154,20 +154,9 @@
|
||||
"Login Failed": "登录失败",
|
||||
"Login Problems": "登录遇到问题?",
|
||||
"Get Help": "获取帮助",
|
||||
"Server Configuration": "服务器配置",
|
||||
"Server URL": "服务器地址",
|
||||
"Enter complete server URL (e.g., http://localhost:9000)": "请输入完整的服务器地址(例如:http://localhost:9000)",
|
||||
"Server URL is required": "服务器地址不能为空",
|
||||
"Only HTTP and HTTPS protocols are supported": "仅支持 HTTP 和 HTTPS 协议",
|
||||
"Invalid hostname": "无效的主机名",
|
||||
"Invalid URL format": "无效的URL格式",
|
||||
"Protocol": "协议",
|
||||
"Host": "主机",
|
||||
"Port": "端口",
|
||||
"Server configuration saved": "服务器配置已保存",
|
||||
"Save Configuration": "保存配置",
|
||||
"Clear Configuration": "清除配置",
|
||||
"Generated Configuration": "生成的配置",
|
||||
"Current Configuration": "当前配置",
|
||||
"Information": "信息",
|
||||
"The system automatically uses the current host as the API base URL. No manual configuration is required.": "系统自动使用当前主机作为API基础URL,无需手动配置。",
|
||||
"Create Bucket": "创建存储桶",
|
||||
"Creation Date": "创建时间",
|
||||
"Bucket Not Empty": "当前桶不为空,请先删除桶内容",
|
||||
@@ -553,8 +542,7 @@
|
||||
"Expire": "过期",
|
||||
"On": "开启",
|
||||
"Off": "关闭",
|
||||
"No valid server configuration detected, please check server configuration": "未检测到有效服务器配置,请检查服务器配置",
|
||||
"Login failed, please check server configuration": "登录失败,请检查服务器配置",
|
||||
|
||||
"Click to modify": "点击修改",
|
||||
"Replicate existing objects": "复制现有对象",
|
||||
"Sync metadata": "同步元数据",
|
||||
@@ -564,5 +552,35 @@
|
||||
"Please enter health check duration": "请输入健康检查时长",
|
||||
"Please enter bandwidth": "请输入带宽",
|
||||
"Please enter storage type": "请输入存储类型",
|
||||
"Add Replication Rule": "添加复制规则"
|
||||
"Add Replication Rule": "添加复制规则",
|
||||
|
||||
"Server Configuration": "服务器配置",
|
||||
"Please configure your RustFS server address": "请配置您的RustFS服务器地址",
|
||||
"Server Address": "服务器地址",
|
||||
"Please enter server address (e.g., http://localhost:9000)": "请输入服务器地址(例如:http://localhost:9000)",
|
||||
"Example: http://localhost:9000 or https://your-domain.com": "示例:http://localhost:9000 或 https://your-domain.com",
|
||||
"Save Configuration": "保存配置",
|
||||
"Skip": "跳过",
|
||||
"Need help?": "需要帮助?",
|
||||
"View Documentation": "查看文档",
|
||||
"Need to configure server?": "需要配置服务器?",
|
||||
"Please enter server address": "请输入服务器地址",
|
||||
"Server configuration saved successfully": "服务器配置保存成功",
|
||||
"Invalid server address format": "无效的服务器地址格式",
|
||||
"Settings": "设置",
|
||||
"Current Configuration": "当前配置",
|
||||
"Not configured": "未配置",
|
||||
"API Base URL": "API基础URL",
|
||||
"S3 Endpoint": "S3端点",
|
||||
"S3 Region": "S3区域",
|
||||
"Reset to Default": "重置为默认",
|
||||
"Configuration Information": "配置信息",
|
||||
"Configuration is saved locally in your browser": "配置保存在您的浏览器本地",
|
||||
"Page will refresh automatically after saving configuration": "保存配置后页面将自动刷新",
|
||||
"Make sure the server address is accessible from your network": "确保服务器地址在您的网络中可访问",
|
||||
"Failed to load current configuration": "加载当前配置失败",
|
||||
"Configuration saved successfully": "配置保存成功",
|
||||
"Configuration reset successfully": "配置重置成功",
|
||||
"Reset to default successfully": "重置为默认成功",
|
||||
"Leave empty to use current host as default": "留空表示使用当前主机作为默认值"
|
||||
}
|
||||
|
||||
+32
-40
@@ -1,16 +1,30 @@
|
||||
import { joinURL } from "ufo";
|
||||
import type { AwsClient } from "./aws4fetch";
|
||||
import { parseApiError } from "~/utils/error-handler";
|
||||
import { logger } from "~/utils/logger";
|
||||
|
||||
interface ApiClientOptions {
|
||||
baseUrl?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
params?: Record<string, string>;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private $api: any;
|
||||
private config?: { baseUrl?: string; headers?: Record<string, string> };
|
||||
private $api: AwsClient;
|
||||
private config?: ApiClientOptions;
|
||||
|
||||
constructor(api: AwsClient, options?: any) {
|
||||
constructor(api: AwsClient, options?: ApiClientOptions) {
|
||||
this.$api = api;
|
||||
this.config = options;
|
||||
}
|
||||
|
||||
async request(url: string, options: any = {}, parseJson: boolean = true) {
|
||||
async request(url: string, options: RequestOptions = {}, parseJson: boolean = true) {
|
||||
url = this.config?.baseUrl ? joinURL(this.config?.baseUrl, url) : url;
|
||||
options.headers = { ...this.config?.headers, ...options.headers };
|
||||
// 处理body的数据格式
|
||||
@@ -22,37 +36,15 @@ class ApiClient {
|
||||
delete options.params; // 删除params,以免影响fetch的options
|
||||
}
|
||||
|
||||
console.log("[request] url:", url);
|
||||
console.log("[request] options:", options);
|
||||
logger.log("[request] url:", url);
|
||||
logger.log("[request] options:", options);
|
||||
|
||||
const response = await this.$api.fetch(url, options);
|
||||
|
||||
console.log("[request] response:", response);
|
||||
logger.log("[request] response:", response);
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMsg = response.statusText;
|
||||
try {
|
||||
// 优先尝试解析为 JSON
|
||||
const errorData = await response.clone().json();
|
||||
errorMsg = errorData.message || JSON.stringify(errorData) || errorMsg;
|
||||
} catch (e) {
|
||||
try {
|
||||
// 如果不是 JSON,尝试解析为文本
|
||||
const text = await response.clone().text();
|
||||
if (text) {
|
||||
// 检查是否为 XML
|
||||
if (text.trim().startsWith("<")) {
|
||||
// 简单提取 <Message> 或 <Error> 标签内容
|
||||
const match = text.match(/<Message>(.*?)<\/Message>/i) || text.match(/<Error>(.*?)<\/Error>/i);
|
||||
errorMsg = match ? match[1] : text;
|
||||
} else {
|
||||
errorMsg = text;
|
||||
}
|
||||
}
|
||||
} catch (e2) {
|
||||
// 保持原有 statusText
|
||||
}
|
||||
}
|
||||
const errorMsg = await parseApiError(response);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
@@ -78,7 +70,7 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
async *streamRequest(url: string, options: any = {}) {
|
||||
async *streamRequest(url: string, options: RequestOptions = {}) {
|
||||
const response = await this.request(url, options, false);
|
||||
|
||||
if (!response.body) {
|
||||
@@ -96,39 +88,39 @@ class ApiClient {
|
||||
const data = JSON.parse(chunk);
|
||||
yield data; // 使用 yield 返回数据
|
||||
} catch (error) {
|
||||
console.error("Failed to parse chunk:", error);
|
||||
logger.error("Failed to parse chunk:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
async get(url: string, options?: any) {
|
||||
async get(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: "GET", ...options });
|
||||
}
|
||||
|
||||
async post(url: string, body: any, options?: any) {
|
||||
async post(url: string, body: any, options?: RequestOptions) {
|
||||
return this.request(url, { method: "POST", body, ...options });
|
||||
}
|
||||
|
||||
async delete(url: string, options?: any) {
|
||||
async delete(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: "DELETE", ...options });
|
||||
}
|
||||
|
||||
async put(url: string, body: any, options?: any) {
|
||||
async put(url: string, body: any, options?: RequestOptions) {
|
||||
return this.request(url, { method: "PUT", body, ...options });
|
||||
}
|
||||
|
||||
async patch(url: string, body: any, options?: any) {
|
||||
async patch(url: string, body: any, options?: RequestOptions) {
|
||||
return this.request(url, { method: "PATCH", body, ...options });
|
||||
}
|
||||
|
||||
async head(url: string, options?: any) {
|
||||
async head(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: "HEAD", ...options });
|
||||
}
|
||||
|
||||
async options(url: string, options?: any) {
|
||||
async options(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: "OPTIONS", ...options });
|
||||
}
|
||||
|
||||
async trace(url: string, options?: any) {
|
||||
async trace(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: "TRACE", ...options });
|
||||
}
|
||||
}
|
||||
|
||||
+15
-12
@@ -1,6 +1,9 @@
|
||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@aws-sdk/types";
|
||||
import type { SiteConfig } from "~/types/config";
|
||||
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts'
|
||||
import type {
|
||||
AwsCredentialIdentity,
|
||||
AwsCredentialIdentityProvider
|
||||
} from '@aws-sdk/types'
|
||||
import type { SiteConfig } from '~/types/config'
|
||||
|
||||
/**
|
||||
* 获取 STS 临时凭证并返回
|
||||
@@ -15,30 +18,30 @@ export async function getStsToken(
|
||||
roleArn: string,
|
||||
customConfig?: SiteConfig
|
||||
) {
|
||||
const siteConfig = customConfig || (useNuxtApp().$siteConfig as SiteConfig);
|
||||
const siteConfig = customConfig || (useNuxtApp().$siteConfig as SiteConfig)
|
||||
|
||||
console.log('S3 siteConfig', siteConfig);
|
||||
console.log('S3 siteConfig', siteConfig)
|
||||
|
||||
// 1. 创建 STS 客户端
|
||||
const stsClient = new STSClient({
|
||||
endpoint: siteConfig.s3.endpoint,
|
||||
region: siteConfig.s3.region || 'us-east-1',
|
||||
credentials: credentials
|
||||
});
|
||||
})
|
||||
|
||||
// 2. 构建 AssumeRole 请求
|
||||
// 你可以根据需要额外添加 DurationSeconds、Policy 等参数
|
||||
const command = new AssumeRoleCommand({
|
||||
RoleArn: roleArn,
|
||||
RoleSessionName: "console", // 自定义角色会话名称
|
||||
DurationSeconds: siteConfig.session?.durationSeconds || 3600 * 12, // 临时凭证有效期
|
||||
});
|
||||
RoleSessionName: 'console', // 自定义角色会话名称
|
||||
DurationSeconds: siteConfig.session?.durationSeconds || 3600 * 12 // 临时凭证有效期
|
||||
})
|
||||
|
||||
const response = await stsClient.send(command);
|
||||
const response = await stsClient.send(command)
|
||||
|
||||
if (!response.Credentials) {
|
||||
throw new Error("Failed to retrieve credentials");
|
||||
throw new Error('Failed to retrieve credentials')
|
||||
}
|
||||
|
||||
return response.Credentials;
|
||||
return response.Credentials
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
const isAuthenticated = useAuth().isAuthenticated.value
|
||||
|
||||
if (to.path === '/auth/login') {
|
||||
if (isAuthenticated) {
|
||||
// 允许访问登录页面和配置页面
|
||||
if (to.path === '/auth/login' || to.path === '/config') {
|
||||
if (isAuthenticated && to.path === '/auth/login') {
|
||||
return navigateTo('/')
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -1,9 +1,9 @@
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
const appName = process.env.APP_NAME || 'RustFS'
|
||||
const baseUrl = (process.env.BASE_URL || '/rustfs/console/').replace(/\/$/, '')
|
||||
const appDescription = process.env.APP_DESCRIPTION || 'RustFS is a distributed file system written in Rust.'
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
@@ -13,20 +13,18 @@ export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-04-13',
|
||||
devtools: { enabled: true },
|
||||
app: {
|
||||
baseURL: baseUrl,
|
||||
head: {
|
||||
title: appName,
|
||||
meta: [
|
||||
{ name: 'description', content: appDescription },
|
||||
],
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
||||
// <link rel="icon" type = "image/png" sizes = "32x32" href = "/favicon-32x32.png" >
|
||||
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' },
|
||||
// <link rel="icon" type = "image/png" sizes = "16x16" href = "/favicon-16x16.png" >
|
||||
{ rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' },
|
||||
// <link rel="apple-touch-icon" sizes = "180x180" href = "/apple-touch-icon.png" >
|
||||
{ rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' },
|
||||
{ rel: 'manifest', href: '/site.webmanifest' },
|
||||
{ rel: 'icon', type: 'image/x-icon', href: baseUrl + '/favicon.ico' },
|
||||
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: baseUrl + '/favicon-32x32.png' },
|
||||
{ rel: 'icon', type: 'image/png', sizes: '16x16', href: baseUrl + '/favicon-16x16.png' },
|
||||
{ rel: 'apple-touch-icon', sizes: '180x180', href: baseUrl + '/apple-touch-icon.png' },
|
||||
{ rel: 'manifest', href: baseUrl + '/site.webmanifest' },
|
||||
],
|
||||
charset: 'utf-8',
|
||||
viewport: 'width=device-width, initial-scale=1',
|
||||
@@ -46,7 +44,9 @@ export default defineNuxtConfig({
|
||||
durationSeconds: Number(process.env.SESSION_DURATION_SECONDS) || 3600 * 12
|
||||
},
|
||||
|
||||
// 下面部分修改的时候记得同步修改 public/config.json
|
||||
// 服务器地址
|
||||
serverHost: process.env.SERVER_HOST || '',
|
||||
|
||||
// admin API 请求基础 URL
|
||||
api: {
|
||||
baseURL: process.env.API_BASE_URL || ''
|
||||
|
||||
Generated
+8
-36
@@ -9,13 +9,13 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.750.0",
|
||||
"@aws-sdk/client-sts": "^3.777.0",
|
||||
"@aws-sdk/protocol-http": "^3.374.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.715.0",
|
||||
"@nuxt/icon": "^1.9.1",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxtjs/i18n": "^9.1.1",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@pinia/nuxt": "^0.9.0",
|
||||
"@smithy/protocol-http": "^5.1.2",
|
||||
"@tanstack/vue-table": "^8.21.2",
|
||||
"@types/three": "^0.175.0",
|
||||
"@vueuse/integrations": "^12.0.0",
|
||||
@@ -777,38 +777,6 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/protocol-http": {
|
||||
"version": "3.374.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/protocol-http": "^1.1.0",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/protocol-http/node_modules/@smithy/protocol-http": {
|
||||
"version": "1.2.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^1.2.0",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/protocol-http/node_modules/@smithy/types": {
|
||||
"version": "1.2.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/region-config-resolver": {
|
||||
"version": "3.775.0",
|
||||
"license": "Apache-2.0",
|
||||
@@ -3694,10 +3662,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/protocol-http": {
|
||||
"version": "5.1.0",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz",
|
||||
"integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.2.0",
|
||||
"@smithy/types": "^4.3.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3782,7 +3752,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz",
|
||||
"integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
|
||||
+34
-29
@@ -7,63 +7,68 @@
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
"postinstall": "nuxt prepare",
|
||||
"lint": "vue-tsc --noEmit && prettier --check .",
|
||||
"lint:fix": "prettier --write .",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.750.0",
|
||||
"@aws-sdk/client-sts": "^3.777.0",
|
||||
"@aws-sdk/protocol-http": "^3.374.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.715.0",
|
||||
"@nuxt/icon": "^1.9.1",
|
||||
"@aws-sdk/client-s3": "^3.844.0",
|
||||
"@aws-sdk/client-sts": "^3.844.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.844.0",
|
||||
"@nuxt/icon": "^1.15.0",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxtjs/i18n": "^9.1.1",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@nuxtjs/i18n": "^9.5.6",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@pinia/nuxt": "^0.9.0",
|
||||
"@tanstack/vue-table": "^8.21.2",
|
||||
"@smithy/protocol-http": "^5.1.2",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@types/three": "^0.175.0",
|
||||
"@vueuse/integrations": "^12.0.0",
|
||||
"@vueuse/integrations": "^12.8.2",
|
||||
"aws-sdk": "^2.1692.0",
|
||||
"axios": "^1.7.9",
|
||||
"axios": "^1.10.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotted-map": "^2.2.3",
|
||||
"echarts": "^5.6.0",
|
||||
"json-editor-vue": "^0.18.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-vue-next": "^0.487.0",
|
||||
"motion-v": "^1.0.0-beta.2",
|
||||
"motion-v": "^1.5.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"nuxt": "^3.16.2",
|
||||
"nuxt": "^3.17.6",
|
||||
"nuxtjs-naive-ui": "^1.0.2",
|
||||
"pinia": "^2.3.0",
|
||||
"postprocessing": "^6.37.2",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
"pinia": "^2.3.1",
|
||||
"postprocessing": "^6.37.6",
|
||||
"strip-json-comments": "^5.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"three": "^0.175.0",
|
||||
"three-globe": "^2.42.3",
|
||||
"ufo": "^1.5.4",
|
||||
"three-globe": "^2.43.0",
|
||||
"ufo": "^1.6.1",
|
||||
"universal-cookie": "^7.2.2",
|
||||
"vue": "^3.5.11",
|
||||
"vue-router": "^4.0.12"
|
||||
"vue": "^3.5.17",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/ri": "^1.2.3",
|
||||
"@aws-sdk/types": "^3.840.0",
|
||||
"@iconify-json/ri": "^1.2.5",
|
||||
"@inspira-ui/plugins": "^0.0.1",
|
||||
"@types/aws4": "^1.11.6",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@vueuse/nuxt": "^13.1.0",
|
||||
"@types/node-forge": "^1.3.12",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"@vueuse/nuxt": "^13.5.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"naive-ui": "^2.40.3",
|
||||
"lodash": "^4.17.21",
|
||||
"naive-ui": "^2.42.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-auto-import": "^0.18.6",
|
||||
"unplugin-vue-components": "^0.27.5",
|
||||
"vfonts": "^0.0.3",
|
||||
"vue-tsc": "^2.1.10"
|
||||
"vue-tsc": "^2.2.12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "4.9.5"
|
||||
|
||||
+11
-273
@@ -4,7 +4,6 @@ await setPageLayout('plain')
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { configManager } from '~/utils/config'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
@@ -22,242 +21,26 @@ const sts = ref({
|
||||
sessionToken: '',
|
||||
})
|
||||
|
||||
const serverConfig = ref({
|
||||
protocol: 'http',
|
||||
host: 'localhost',
|
||||
port: '9000',
|
||||
region: 'us-east-1'
|
||||
})
|
||||
|
||||
|
||||
const message = useMessage()
|
||||
const auth = useAuth()
|
||||
|
||||
// 控制服务器配置折叠面板展开
|
||||
const expandedNames = ref<string[]>([])
|
||||
// 警告提示内容
|
||||
const configAlert = ref('')
|
||||
|
||||
// 检查是否需要默认展开服务器配置
|
||||
onMounted(async () => {
|
||||
// 使用 configManager 加载配置(按优先级:config.json > localStorage > 默认值)
|
||||
const config = await configManager.loadConfig()
|
||||
if (config) {
|
||||
const serverConfigData = configManager.extractServerConfig(config)
|
||||
if (serverConfigData) {
|
||||
// 更新 serverConfig
|
||||
serverConfig.value = serverConfigData
|
||||
|
||||
// 更新 serverUrl
|
||||
serverUrl.value = `${serverConfigData.protocol}://${serverConfigData.host}:${serverConfigData.port}`
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有有效配置,设置默认 serverUrl
|
||||
if (!serverUrl.value) {
|
||||
serverUrl.value = `${serverConfig.value.protocol}://${serverConfig.value.host}:${serverConfig.value.port}`
|
||||
}
|
||||
|
||||
if (route.query.showConfig === '1') {
|
||||
expandedNames.value = ['server-config']
|
||||
configAlert.value = t('No valid server configuration detected, please check server configuration')
|
||||
}
|
||||
})
|
||||
|
||||
const handleLogin = async () => {
|
||||
// 验证并解析URL
|
||||
validateAndParseUrl()
|
||||
if (urlValidationStatus.value === 'error') {
|
||||
expandedNames.value = ['server-config']
|
||||
return
|
||||
}
|
||||
|
||||
const credentials = method.value === 'accessKeyAndSecretKey' ? accessKeyAndSecretKey.value : sts.value
|
||||
|
||||
try {
|
||||
// 构建当前配置
|
||||
const nuxtApp = useNuxtApp()
|
||||
const baseConfig = nuxtApp.$siteConfig
|
||||
const currentConfig = {
|
||||
...baseConfig,
|
||||
api: {
|
||||
baseURL: `${serverConfig.value.protocol}://${serverConfig.value.host}:${serverConfig.value.port}/rustfs/admin/v3`
|
||||
},
|
||||
s3: {
|
||||
endpoint: `${serverConfig.value.protocol}://${serverConfig.value.host}:${serverConfig.value.port}`,
|
||||
region: serverConfig.value.region
|
||||
}
|
||||
}
|
||||
// 使用配置管理器获取配置(优先使用localStorage中的配置)
|
||||
const { configManager } = await import('~/utils/config')
|
||||
const currentConfig = await configManager.loadConfig()
|
||||
|
||||
await auth.login(credentials, currentConfig)
|
||||
|
||||
// 登录成功后重新初始化插件,确保使用新的配置
|
||||
await reinitializePlugins()
|
||||
|
||||
message.success(t('Login Success'))
|
||||
// 重新加载页面以确保新的配置生效
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
expandedNames.value = ['server-config']
|
||||
configAlert.value = t('Login failed, please check server configuration')
|
||||
message.error(t('Login Failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const serverUrl = ref('')
|
||||
const urlValidationStatus = ref<'success' | 'warning' | 'error' | undefined>(undefined)
|
||||
const urlError = ref('')
|
||||
|
||||
const validateAndParseUrl = () => {
|
||||
const url = serverUrl.value.trim()
|
||||
|
||||
if (!url) {
|
||||
urlValidationStatus.value = 'error'
|
||||
urlError.value = t('Server URL is required')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
|
||||
// 验证协议
|
||||
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
||||
urlValidationStatus.value = 'error'
|
||||
urlError.value = t('Only HTTP and HTTPS protocols are supported')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证主机
|
||||
if (!urlObj.hostname) {
|
||||
urlValidationStatus.value = 'error'
|
||||
urlError.value = t('Invalid hostname')
|
||||
return
|
||||
}
|
||||
|
||||
// 解析并更新配置
|
||||
serverConfig.value.protocol = urlObj.protocol.replace(':', '')
|
||||
serverConfig.value.host = urlObj.hostname
|
||||
serverConfig.value.port = urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80')
|
||||
|
||||
urlValidationStatus.value = 'success'
|
||||
urlError.value = ''
|
||||
|
||||
// 立即保存配置并更新 $siteConfig
|
||||
saveConfigAndUpdateSiteConfig()
|
||||
} catch (error) {
|
||||
urlValidationStatus.value = 'error'
|
||||
urlError.value = t('Invalid URL format')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置并更新 $siteConfig 的函数
|
||||
const saveConfigAndUpdateSiteConfig = async () => {
|
||||
try {
|
||||
// 保存配置到 localStorage
|
||||
const saved = await configManager.saveConfig({
|
||||
protocol: serverConfig.value.protocol,
|
||||
host: serverConfig.value.host,
|
||||
port: serverConfig.value.port,
|
||||
region: serverConfig.value.region
|
||||
})
|
||||
|
||||
if (saved) {
|
||||
// 动态更新 $siteConfig
|
||||
const nuxtApp = useNuxtApp()
|
||||
const currentConfig = nuxtApp.$siteConfig
|
||||
const updatedConfig = {
|
||||
...currentConfig,
|
||||
api: {
|
||||
baseURL: `${serverConfig.value.protocol}://${serverConfig.value.host}:${serverConfig.value.port}/rustfs/admin/v3`
|
||||
},
|
||||
s3: {
|
||||
endpoint: `${serverConfig.value.protocol}://${serverConfig.value.host}:${serverConfig.value.port}`,
|
||||
region: serverConfig.value.region
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 $siteConfig
|
||||
nuxtApp.$siteConfig = updatedConfig
|
||||
|
||||
// 重新初始化插件以使用新的配置
|
||||
await reinitializePlugins()
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to save config:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 重新初始化插件的函数
|
||||
const reinitializePlugins = async () => {
|
||||
try {
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
// 清除现有的客户端
|
||||
if ('$api' in nuxtApp) {
|
||||
delete (nuxtApp as any).$api
|
||||
}
|
||||
if ('$s3Client' in nuxtApp) {
|
||||
delete (nuxtApp as any).$s3Client
|
||||
}
|
||||
|
||||
// 重新运行插件
|
||||
// 注意:这里我们需要手动重新创建客户端,因为插件只在应用启动时运行一次
|
||||
const { credentials, isAuthenticated } = useAuth()
|
||||
|
||||
if (isAuthenticated.value && credentials.value) {
|
||||
const siteConfig = nuxtApp.$siteConfig
|
||||
|
||||
// 重新创建 API 客户端
|
||||
const { AwsClient } = await import('~/lib/aws4fetch')
|
||||
const ApiClient = (await import('~/lib/api-client')).default
|
||||
|
||||
const adminApiClient = new AwsClient({
|
||||
accessKeyId: credentials.value.AccessKeyId || '',
|
||||
secretAccessKey: credentials.value.SecretAccessKey || '',
|
||||
sessionToken: credentials.value.SessionToken || '',
|
||||
region: siteConfig.s3.region || 'us-east-1',
|
||||
service: 's3'
|
||||
})
|
||||
|
||||
nuxtApp.$api = new ApiClient(adminApiClient, {
|
||||
baseUrl: siteConfig.api.baseURL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 重新创建 S3 客户端
|
||||
const { S3Client } = await import('@aws-sdk/client-s3')
|
||||
|
||||
nuxtApp.$s3Client = new S3Client({
|
||||
endpoint: siteConfig.s3.endpoint,
|
||||
region: siteConfig.s3.region || 'us-east-1',
|
||||
forcePathStyle: true,
|
||||
requestChecksumCalculation: 'WHEN_REQUIRED',
|
||||
credentials: {
|
||||
accessKeyId: credentials.value.AccessKeyId || '',
|
||||
secretAccessKey: credentials.value.SecretAccessKey || '',
|
||||
sessionToken: credentials.value.SessionToken || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to reinitialize plugins:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 region 变化
|
||||
const onRegionChange = () => {
|
||||
saveConfigAndUpdateSiteConfig()
|
||||
}
|
||||
|
||||
const onUrlInput = () => {
|
||||
// 清除之前的错误状态
|
||||
if (urlValidationStatus.value === 'error') {
|
||||
urlValidationStatus.value = undefined
|
||||
urlError.value = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -268,6 +51,13 @@ const onUrlInput = () => {
|
||||
<auth-heros-static></auth-heros-static>
|
||||
</div>
|
||||
<div class="w-full lg:w-1/2 flex flex-col justify-center items-center bg-white dark:bg-neutral-900 dark:border-neutral-700 relative">
|
||||
<!-- 表单容器右上角的配置按钮 -->
|
||||
<div class="absolute top-4 right-4 z-10">
|
||||
<NuxtLink to="/config" class="inline-flex items-center justify-center w-8 h-8 rounded-lg border border-gray-200 bg-white text-gray-600 shadow-sm hover:bg-gray-50 hover:text-gray-800 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-white transition-colors" :title="t('Server Configuration')">
|
||||
<Icon name="ri:settings-3-line" class="text-sm" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="max-w-sm w-full p-4 sm:p-7">
|
||||
<img src="https://rustfs.com/rustfs.logo.svg" class="max-w-28" alt="" />
|
||||
<div class="py-6">
|
||||
@@ -315,58 +105,6 @@ const onUrlInput = () => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 服务器配置折叠面板 -->
|
||||
<div class="mb-2">
|
||||
<div class="transition-all duration-300 ease-in-out">
|
||||
<!-- 折叠时的卡片 -->
|
||||
<div v-if="!expandedNames.includes('server-config')" @click="expandedNames = ['server-config']" class="w-full cursor-pointer">
|
||||
<div
|
||||
class="rounded border border-gray-200 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 px-4 py-2 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-neutral-700 transition-colors">
|
||||
<div class="truncate">
|
||||
<span class="font-mono text-sm">{{ serverConfig.protocol }}://{{ serverConfig.host }}:{{ serverConfig.port }}</span>
|
||||
<span class="ml-2 text-xs text-gray-500">{{ serverConfig.region }}</span>
|
||||
</div>
|
||||
<Icon :size="18" name="ri:arrow-right-s-fill" class="text-gray-600 dark:text-gray-300 hover:text-blue-500 transition-all duration-200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开时的内容 -->
|
||||
<div v-else class="border border-gray-200 dark:border-neutral-700 rounded-lg overflow-hidden animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<!-- 标题栏 -->
|
||||
<div @click="expandedNames = []"
|
||||
class="w-full cursor-pointer flex items-center justify-between px-4 py-3 bg-gray-50 dark:bg-neutral-800 hover:bg-gray-100 dark:hover:bg-neutral-700 transition-colors border-b border-gray-200 dark:border-neutral-700">
|
||||
<span class="flex items-center text-sm font-medium">
|
||||
<Icon :size="16" name="ri:settings-3-line" class="mr-2 text-gray-500" />
|
||||
{{ t('Server Configuration') }}
|
||||
</span>
|
||||
<Icon :size="18" name="ri:arrow-down-s-fill" class="text-gray-600 dark:text-gray-300 hover:text-blue-500 transition-all duration-200 transform" />
|
||||
</div>
|
||||
|
||||
<!-- 配置表单 -->
|
||||
<div class="p-4 bg-white dark:bg-neutral-900">
|
||||
<div v-if="configAlert"
|
||||
class="mb-4 px-3 py-2 rounded bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-200 dark:border-red-800 text-sm">
|
||||
{{ configAlert }}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">{{ t('Server URL') }}</label>
|
||||
<n-input v-model:value="serverUrl" size="medium" type="text" :placeholder="'http://localhost:9000'" :status="urlValidationStatus"
|
||||
@blur="validateAndParseUrl" @input="onUrlInput" />
|
||||
<div v-if="urlError" class="mt-1 text-xs text-red-500">{{ urlError }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2 text-gray-700 dark:text-gray-300">{{ t('Region') }}</label>
|
||||
<n-input v-model:value="serverConfig.region" size="medium" type="text" :placeholder="'us-east-1'" @change="onRegionChange" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
|
||||
{{ t('Login') }}
|
||||
@@ -378,7 +116,7 @@ const onUrlInput = () => {
|
||||
|
||||
<div class="my-8">
|
||||
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
||||
{{ t('Login Problems?') }} <nuxt-link to="https://www.rustfs.com" class="text-blue-600 hover:underline">{{ t('Get Help') }}</nuxt-link>
|
||||
{{ t('Login Problems?') }} <NuxtLink to="https://www.rustfs.com" class="text-blue-600 hover:underline">{{ t('Get Help') }}</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
<script lang="ts" setup>
|
||||
await setPageLayout('plain')
|
||||
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const serverHost = ref('')
|
||||
const isValid = ref(false)
|
||||
|
||||
const validateAndSave = async () => {
|
||||
try {
|
||||
if (serverHost.value) {
|
||||
// 更宽松的URL验证
|
||||
let urlToValidate = serverHost.value.trim()
|
||||
|
||||
// 如果没有协议,自动添加https://
|
||||
if (!urlToValidate.match(/^https?:\/\//)) {
|
||||
urlToValidate = 'https://' + urlToValidate
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
const url = new URL(urlToValidate)
|
||||
console.log('Valid URL:', url.href) // 调试信息
|
||||
|
||||
// 保存原始输入(如果用户没输入协议,就保存添加了协议的版本)
|
||||
const urlToSave = serverHost.value.match(/^https?:\/\//) ? serverHost.value : urlToValidate
|
||||
localStorage.setItem('rustfs-server-host', urlToSave)
|
||||
|
||||
// 如果我们自动添加了协议,更新输入框显示
|
||||
if (!serverHost.value.match(/^https?:\/\//)) {
|
||||
serverHost.value = urlToValidate
|
||||
}
|
||||
} else {
|
||||
// 如果为空,清除localStorage,使用默认值
|
||||
localStorage.removeItem('rustfs-server-host')
|
||||
}
|
||||
|
||||
// 清除配置缓存
|
||||
const { configManager } = await import('~/utils/config')
|
||||
configManager.clearCache()
|
||||
|
||||
message.success(t('Server configuration saved successfully'))
|
||||
|
||||
// 延迟后刷新页面并跳转,确保配置完全生效
|
||||
setTimeout(() => {
|
||||
window.location.href = '/auth/login'
|
||||
}, 200)
|
||||
} catch (error) {
|
||||
console.error('URL validation error:', error) // 调试信息
|
||||
message.error(t('Invalid server address format') + ': ' + (error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
const resetToCurrentHost = async () => {
|
||||
// 清除localStorage中的配置,让系统回到默认状态(使用当前host)
|
||||
localStorage.removeItem('rustfs-server-host')
|
||||
|
||||
// 清除配置缓存
|
||||
const { configManager } = await import('~/utils/config')
|
||||
configManager.clearCache()
|
||||
|
||||
// 清空输入框,表示使用默认配置
|
||||
serverHost.value = ''
|
||||
|
||||
message.success(t('Reset to default successfully'))
|
||||
|
||||
// 延迟后跳转到登录页面,确保配置完全生效
|
||||
setTimeout(() => {
|
||||
window.location.href = '/auth/login'
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const skipConfig = () => {
|
||||
router.push('/auth/login')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查是否已有配置
|
||||
const saved = localStorage.getItem('rustfs-server-host')
|
||||
if (saved) {
|
||||
serverHost.value = saved
|
||||
isValid.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="lg:p-20 flex flex-col items-center justify-center min-h-screen bg-gray-100 dark:bg-neutral-800">
|
||||
<img src="~/assets/backgrounds/scillate.svg" class="absolute inset-0 z-0 opacity-45" alt="" />
|
||||
<div class="flex-1 flex w-full z-10 max-w-7xl lg:max-h-[85vh] shadow-lg rounded-lg overflow-hidden mx-auto dark:bg-neutral-800 dark:border-neutral-700">
|
||||
<div class="hidden lg:block w-1/2">
|
||||
<auth-heros-static></auth-heros-static>
|
||||
</div>
|
||||
<div class="w-full lg:w-1/2 flex flex-col justify-center items-center bg-white dark:bg-neutral-900 dark:border-neutral-700 relative">
|
||||
<div class="max-w-sm w-full p-4 sm:p-7">
|
||||
<img src="https://rustfs.com/rustfs.logo.svg" class="max-w-28" alt="" />
|
||||
<div class="py-6">
|
||||
<h1 class="block text-2xl font-bold text-gray-800 dark:text-white">{{ t('Server Configuration') }}</h1>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
|
||||
{{ t('Please configure your RustFS server address') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 space-y-4">
|
||||
<!-- Form -->
|
||||
<form @submit.prevent="validateAndSave" autocomplete="off">
|
||||
<div class="grid gap-y-6">
|
||||
<div>
|
||||
<label for="serverHost" class="block text-sm mb-2 dark:text-white">{{ t('Server Address') }}</label>
|
||||
<div class="text-xs text-gray-500 mb-2">
|
||||
{{ t('Leave empty to use current host as default') }}
|
||||
</div>
|
||||
<n-input
|
||||
v-model:value="serverHost"
|
||||
type="text"
|
||||
:placeholder="t('Please enter server address (e.g., http://localhost:9000)')"
|
||||
/>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{{ t('Example: http://localhost:9000 or https://your-domain.com') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit"
|
||||
class="flex-1 py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
|
||||
{{ t('Save Configuration') }}
|
||||
</button>
|
||||
|
||||
<button type="button" @click="resetToCurrentHost"
|
||||
class="py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800">
|
||||
{{ t('Reset') }}
|
||||
</button>
|
||||
|
||||
<button type="button" @click="skipConfig"
|
||||
class="py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800">
|
||||
{{ t('Skip') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<!-- End Form -->
|
||||
</div>
|
||||
|
||||
<div class="my-8">
|
||||
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
||||
{{ t('Need help?') }} <NuxtLink to="https://docs.rustfs.com" class="text-blue-600 hover:underline">{{ t('View Documentation') }}</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-around gap-4 w-1/2 mx-auto">
|
||||
<div class="inline-flex">
|
||||
<theme-switcher />
|
||||
</div>
|
||||
<div class="inline-flex">
|
||||
<language-switcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="license-article">
|
||||
<page-header>
|
||||
<template #title>
|
||||
<h1 class="text-2xl font-bold">{{ t('License') }}</h1>
|
||||
</template>
|
||||
</page-header>
|
||||
|
||||
<page-content>
|
||||
<n-card size="large">
|
||||
<template #header>
|
||||
<h1 class="text-2xl font-bold text-center">{{ t('Apache License') }}</h1>
|
||||
</template>
|
||||
|
||||
<article class="max-w-4xl mx-auto">
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<n-spin size="large" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="text-center py-8">
|
||||
<n-alert type="error" :title="t('Failed to load license')">
|
||||
{{ error }}
|
||||
</n-alert>
|
||||
</div>
|
||||
|
||||
<div v-else class="license-content">
|
||||
<pre class="whitespace-pre-wrap font-mono text-sm leading-6 bg-gray-50 dark:bg-gray-800 p-6 rounded-lg overflow-auto">{{ licenseContent }}</pre>
|
||||
</div>
|
||||
</article>
|
||||
</n-card>
|
||||
</page-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const licenseContent = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 从public目录读取LICENSE文件
|
||||
const response = await fetch('/LICENSE')
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
licenseContent.value = await response.text()
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Unknown error'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.license-content {
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,7 @@
|
||||
<!-- 顶部信息 -->
|
||||
<n-card>
|
||||
<n-flex justify="space-between" class="top-info py-4 px-2">
|
||||
<n-space vertical v-if="new Date().getTime() - siteConfig.license.expired*1000<0">
|
||||
<n-space vertical v-if="false">
|
||||
<n-space><n-tag type="success">{{ t('Enterprise License') }}</n-tag> <n-text style="line-height: 28px;" type="success">{{ t('Status') }}:{{ t('Normal') }}</n-text></n-space>
|
||||
<n-space>{{ t('License Valid Until') }}:{{ endDate }}</n-space>
|
||||
</n-space>
|
||||
@@ -44,7 +44,7 @@
|
||||
<!-- 许可证详情 -->
|
||||
<n-card :title="t('License Details')" class="license-details mt-4">
|
||||
<n-descriptions :columns="2" :bordered="true">
|
||||
<n-descriptions-item :label="t('Licensed Company')">{{ siteConfig.license?.name }}</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Licensed Company')">{{ t('No License') }}</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('License Key')">{{ licenseKey }}</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Licensed Users')">{{ t('Unlimited') }}</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('Support Level')">{{ t('Enterprise') }} (7x24x365)</n-descriptions-item>
|
||||
@@ -128,7 +128,7 @@
|
||||
</page-content >
|
||||
</div>
|
||||
|
||||
<articleL v-else></articleL>
|
||||
<LicenseArticle v-else></LicenseArticle>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -138,16 +138,17 @@ import { NButton } from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { SiteConfig } from "~/types/config"
|
||||
import articleL from './article.vue'
|
||||
import LicenseArticle from './components/license-article.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const siteConfig = useNuxtApp().$siteConfig as SiteConfig;
|
||||
const hasLicense = siteConfig.license
|
||||
// 由于移除了license配置,始终显示无许可证状态
|
||||
const hasLicense = false
|
||||
|
||||
// 服务开始
|
||||
const startDate = new Date('2025-01-01');
|
||||
// 服务结束
|
||||
const endDate = dayjs(siteConfig.license.expired*1000).format('YYYY-MM-DD');
|
||||
// 服务结束 - 使用默认日期
|
||||
const endDate = dayjs().format('YYYY-MM-DD');
|
||||
|
||||
// 数据
|
||||
const licenseKey = ref('RUSTFS-ENTERPRISE-127-183');
|
||||
@@ -1,162 +0,0 @@
|
||||
<template>
|
||||
<div class="license-page eading-7">
|
||||
<page-header>
|
||||
<template #title>
|
||||
<h1 class="text-2xl font-bold">{{ t('License') }}</h1>
|
||||
</template>
|
||||
</page-header>
|
||||
<page-content>
|
||||
<n-card size="large" >
|
||||
<template #header>
|
||||
<h1 class="text-2xl font-bold text-center">{{ t('Apache License') }}</h1>
|
||||
</template>
|
||||
<article>
|
||||
<section style="max-width: 1220px;margin: 0 auto;">
|
||||
<n-p>
|
||||
{{ t('Version 2.0, January 2004') }}
|
||||
</n-p>
|
||||
<n-p class=" mt-2">
|
||||
<n-a href="http://www.apache.org/licenses/"><n-text type="info">http://www.apache.org/licenses/</n-text></n-a>
|
||||
</n-p>
|
||||
<n-divider />
|
||||
<n-h3>{{ t('Terms and Conditions for Use, Reproduction, and Distribution') }}</n-h3>
|
||||
<n-h4>1. {{ t('Definitions') }}</n-h4>
|
||||
<n-p>
|
||||
{{ t('License definition paragraph') }}
|
||||
</n-p>
|
||||
<n-p>
|
||||
{{ t('Licensor definition paragraph') }}
|
||||
</n-p>
|
||||
<n-p>
|
||||
{{ t('Legal Entity definition paragraph') }}
|
||||
</n-p>
|
||||
<n-p>
|
||||
{{ t('You definition paragraph') }}
|
||||
</n-p>
|
||||
<n-p>
|
||||
{{ t('Source form definition paragraph') }}
|
||||
</n-p>
|
||||
<n-p>
|
||||
{{ t('Object form definition paragraph') }}
|
||||
</n-p>
|
||||
<n-p>
|
||||
{{ t('Work definition paragraph') }}
|
||||
</n-p>
|
||||
<n-p>
|
||||
{{ t('Derivative Works definition paragraph') }}
|
||||
</n-p>
|
||||
<n-p>
|
||||
{{ t('Contribution definition paragraph') }}
|
||||
</n-p>
|
||||
<n-p>
|
||||
{{ t('Contributor definition paragraph') }}
|
||||
</n-p>
|
||||
<n-h4>
|
||||
2. {{ t('Grant of Copyright License') }}
|
||||
</n-h4>
|
||||
<n-p>
|
||||
{{ t('Copyright license paragraph') }}
|
||||
</n-p>
|
||||
<n-h4>
|
||||
3. {{ t('Grant of Patent License') }}
|
||||
</n-h4>
|
||||
<n-p>
|
||||
{{ t('Patent license paragraph') }}
|
||||
</n-p>
|
||||
<n-h4>
|
||||
4. {{ t('Redistribution') }}
|
||||
</n-h4>
|
||||
<n-p>
|
||||
{{ t('Redistribution paragraph') }}
|
||||
</n-p>
|
||||
<n-ol>
|
||||
<n-li>
|
||||
1. {{ t('Redistribution condition 1') }}
|
||||
</n-li>
|
||||
<n-li>
|
||||
2. {{ t('Redistribution condition 2') }}
|
||||
</n-li>
|
||||
<n-li>
|
||||
3. {{ t('Redistribution condition 3') }}
|
||||
</n-li>
|
||||
<n-li>
|
||||
4. {{ t('Redistribution condition 4') }}
|
||||
</n-li>
|
||||
</n-ol>
|
||||
<n-p>
|
||||
{{ t('Additional redistribution paragraph') }}
|
||||
</n-p>
|
||||
<n-h4>
|
||||
5. {{ t('Submission of Contributions') }}
|
||||
</n-h4>
|
||||
<n-p>
|
||||
{{ t('Contributions paragraph') }}
|
||||
</n-p>
|
||||
<n-h4>
|
||||
6. {{ t('Trademarks') }}
|
||||
</n-h4>
|
||||
<n-p>
|
||||
{{ t('Trademarks paragraph') }}
|
||||
</n-p>
|
||||
<n-h4>
|
||||
7. {{ t('Disclaimer of Warranty') }}
|
||||
</n-h4>
|
||||
<n-p>
|
||||
{{ t('Disclaimer paragraph') }}
|
||||
</n-p>
|
||||
<n-h4>
|
||||
8. {{ t('Limitation of Liability') }}
|
||||
</n-h4>
|
||||
<n-p>
|
||||
{{ t('Liability paragraph') }}
|
||||
</n-p>
|
||||
<n-h4>
|
||||
9. {{ t('Accepting Warranty or Additional Liability') }}
|
||||
</n-h4>
|
||||
<n-p>
|
||||
{{ t('Additional liability paragraph') }}
|
||||
</n-p>
|
||||
<n-h4>
|
||||
{{ t('END OF TERMS AND CONDITIONS') }}
|
||||
</n-h4>
|
||||
<n-h4>
|
||||
{{ t('APPENDIX: How to apply the Apache License to your work') }}
|
||||
</n-h4>
|
||||
<n-p>
|
||||
{{ t('Appendix paragraph 1') }}
|
||||
</n-p>
|
||||
<n-p>
|
||||
{{ t('Appendix paragraph 2') }}
|
||||
</n-p>
|
||||
<n-p> {{ t('Copyright RustFS') }}</n-p>
|
||||
<n-p>{{ t('Licensed under Apache 2.0') }}</n-p>
|
||||
<n-p>{{ t('You may not use this file except in compliance with the License') }}</n-p>
|
||||
<n-p>{{ t('You may obtain a copy of the License at') }}</n-p>
|
||||
<n-p>
|
||||
{{ t('Appendix final paragraph') }}
|
||||
</n-p>
|
||||
</section>
|
||||
</article>
|
||||
</n-card>
|
||||
</page-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { NCard, NDivider, NOl } from 'naive-ui';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
article h2, article h3 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
article p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
</style>
|
||||
+143
-86
@@ -1,115 +1,172 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { configManager } from '~/utils/config'
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
// 服务端配置
|
||||
const serverConfig = ref({
|
||||
protocol: 'http',
|
||||
host: 'localhost',
|
||||
port: '9000',
|
||||
region: 'us-east-1'
|
||||
// 当前配置信息
|
||||
const currentConfig = ref({
|
||||
serverHost: '',
|
||||
api: {
|
||||
baseURL: ''
|
||||
},
|
||||
s3: {
|
||||
endpoint: '',
|
||||
region: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
serverHost: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 加载当前配置
|
||||
const loadCurrentConfig = async () => {
|
||||
const currentConfig = await configManager.loadConfig()
|
||||
if (currentConfig) {
|
||||
const serverConfigData = configManager.extractServerConfig(currentConfig)
|
||||
if (serverConfigData) {
|
||||
serverConfig.value = serverConfigData
|
||||
}
|
||||
try {
|
||||
const config = await configManager.loadConfig()
|
||||
currentConfig.value = config
|
||||
formData.value.serverHost = config.serverHost
|
||||
} catch (error) {
|
||||
message.error(t('Failed to load current configuration'))
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
const saved = await configManager.saveConfig(serverConfig.value)
|
||||
if (saved) {
|
||||
message.success(t('Server configuration saved'))
|
||||
} else {
|
||||
message.info(t('Using public configuration, cannot save to localStorage'))
|
||||
if (!formData.value.serverHost) {
|
||||
message.error(t('Please enter server address'))
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 更宽松的URL验证
|
||||
let urlToValidate = formData.value.serverHost.trim()
|
||||
|
||||
// 如果没有协议,自动添加https://
|
||||
if (!urlToValidate.match(/^https?:\/\//)) {
|
||||
urlToValidate = 'https://' + urlToValidate
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
const url = new URL(urlToValidate)
|
||||
|
||||
// 保存原始输入(如果用户没输入协议,就保存添加了协议的版本)
|
||||
const urlToSave = formData.value.serverHost.match(/^https?:\/\//) ? formData.value.serverHost : urlToValidate
|
||||
localStorage.setItem('rustfs-server-host', urlToSave)
|
||||
|
||||
// 如果我们自动添加了协议,更新输入框显示
|
||||
if (!formData.value.serverHost.match(/^https?:\/\//)) {
|
||||
formData.value.serverHost = urlToValidate
|
||||
}
|
||||
|
||||
// 清除配置缓存
|
||||
configManager.clearCache()
|
||||
|
||||
message.success(t('Configuration saved successfully'))
|
||||
|
||||
// 延迟后刷新页面,确保配置完全生效
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 200)
|
||||
} catch (error) {
|
||||
message.error(t('Invalid server address format'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清除配置
|
||||
const clearConfig = async () => {
|
||||
configManager.clearConfig()
|
||||
message.success('Configuration cleared')
|
||||
await loadCurrentConfig()
|
||||
// 重置配置
|
||||
const resetConfig = async () => {
|
||||
localStorage.removeItem('rustfs-server-host')
|
||||
configManager.clearCache()
|
||||
|
||||
message.success(t('Configuration reset successfully'))
|
||||
|
||||
// 延迟后刷新页面,确保配置完全生效
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// 页面加载时读取当前配置
|
||||
onMounted(async () => {
|
||||
await loadCurrentConfig()
|
||||
onMounted(() => {
|
||||
loadCurrentConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="bg-white dark:bg-neutral-800 rounded-lg shadow-lg p-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-6">
|
||||
{{ t('Server Configuration') }}
|
||||
</h1>
|
||||
<div>
|
||||
<page-header>
|
||||
<template #title>
|
||||
<h1 class="text-2xl font-bold">{{ t('Settings') }}</h1>
|
||||
</template>
|
||||
</page-header>
|
||||
|
||||
<page-content>
|
||||
<!-- 当前配置显示 -->
|
||||
<n-card :title="t('Current Configuration')" class="mb-6">
|
||||
<n-descriptions :columns="1" :bordered="true">
|
||||
<n-descriptions-item :label="t('Server Host')">
|
||||
{{ currentConfig.serverHost || t('Not configured') }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('API Base URL')">
|
||||
{{ currentConfig.api.baseURL || t('Not configured') }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('S3 Endpoint')">
|
||||
{{ currentConfig.s3.endpoint || t('Not configured') }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="t('S3 Region')">
|
||||
{{ currentConfig.s3.region || t('Not configured') }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-card>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{{ t('Protocol') }}
|
||||
</label>
|
||||
<n-select v-model:value="serverConfig.protocol" :options="[
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: 'HTTPS', value: 'https' }
|
||||
]" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{{ t('Port') }}
|
||||
</label>
|
||||
<n-input v-model:value="serverConfig.port" type="text" :placeholder="'9000'" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 配置表单 -->
|
||||
<n-card :title="t('Server Configuration')">
|
||||
<n-form @submit.prevent="saveConfig">
|
||||
<n-form-item :label="t('Server Address')" required>
|
||||
<n-input
|
||||
v-model:value="formData.serverHost"
|
||||
:placeholder="t('Please enter server address (e.g., http://localhost:9000)')"
|
||||
clearable
|
||||
/>
|
||||
<template #feedback>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ t('Example: http://localhost:9000 or https://your-domain.com') }}
|
||||
</div>
|
||||
</template>
|
||||
</n-form-item>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{{ t('Host') }}
|
||||
</label>
|
||||
<n-input v-model:value="serverConfig.host" type="text" :placeholder="'localhost'" />
|
||||
</div>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="saveConfig"
|
||||
:loading="loading"
|
||||
>
|
||||
{{ t('Save Configuration') }}
|
||||
</n-button>
|
||||
|
||||
<n-button @click="resetConfig">
|
||||
{{ t('Reset to Default') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{{ t('Region') }}
|
||||
</label>
|
||||
<n-input v-model:value="serverConfig.region" type="text" :placeholder="'us-east-1'" />
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-neutral-700 p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{{ t('Generated Configuration') }}
|
||||
</h3>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<div><strong>API URL:</strong> {{ serverConfig.protocol }}://{{ serverConfig.host }}:{{ serverConfig.port }}/rustfs/admin/v3</div>
|
||||
<div><strong>S3 Endpoint:</strong> {{ serverConfig.protocol }}://{{ serverConfig.host }}:{{ serverConfig.port }}</div>
|
||||
<div><strong>Region:</strong> {{ serverConfig.region }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 pt-4">
|
||||
<n-button type="primary" @click="saveConfig">
|
||||
{{ t('Save Configuration') }}
|
||||
</n-button>
|
||||
<n-button @click="clearConfig">
|
||||
{{ t('Clear Configuration') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<n-alert type="info" :title="t('Configuration Information')" class="mt-4">
|
||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||
<li>{{ t('Configuration is saved locally in your browser') }}</li>
|
||||
<li>{{ t('Page will refresh automatically after saving configuration') }}</li>
|
||||
<li>{{ t('Make sure the server address is accessible from your network') }}</li>
|
||||
</ul>
|
||||
</n-alert>
|
||||
</n-card>
|
||||
</page-content>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
+11
-43
@@ -1,4 +1,5 @@
|
||||
import { configManager } from '~/utils/config'
|
||||
import { logger } from '~/utils/logger'
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'load-site-config',
|
||||
@@ -6,51 +7,18 @@ export default defineNuxtPlugin({
|
||||
let finalConfig: any
|
||||
|
||||
try {
|
||||
// 使用 configManager 加载配置(内部优先级:config.json > localStorage > runtimeconfig)
|
||||
const userConfig = await configManager.loadConfig()
|
||||
if (userConfig) {
|
||||
finalConfig = { ...useRuntimeConfig().public, ...userConfig }
|
||||
console.log('Configuration loaded from configManager')
|
||||
// 使用 configManager 加载配置(优先使用当前host)
|
||||
const config = await configManager.loadConfig()
|
||||
if (config) {
|
||||
finalConfig = { ...useRuntimeConfig().public, ...config }
|
||||
logger.log('Configuration loaded from configManager')
|
||||
} else {
|
||||
// 如果 configManager 没有配置,使用 runtimeConfig
|
||||
finalConfig = useRuntimeConfig().public
|
||||
console.log('Configuration loaded from runtimeConfig')
|
||||
logger.log('Configuration loaded from runtimeConfig')
|
||||
}
|
||||
|
||||
// 如果有 public/config.json,尝试获取额外的信息(如 license)
|
||||
if (await configManager.hasPublicConfig()) {
|
||||
try {
|
||||
const response = await fetch('/config.json')
|
||||
const publicConfig = await response.json()
|
||||
|
||||
// 保留 license 和其他额外信息
|
||||
if (publicConfig.license) {
|
||||
finalConfig.license = publicConfig.license
|
||||
}
|
||||
if (publicConfig.release) {
|
||||
finalConfig.release = publicConfig.release
|
||||
}
|
||||
if (publicConfig.doc) {
|
||||
finalConfig.doc = publicConfig.doc
|
||||
}
|
||||
|
||||
// 获取 license 信息(如果不是开发环境)
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
try {
|
||||
const licenseResponse = await fetch('/license')
|
||||
const licenseConfig = await licenseResponse.json()
|
||||
finalConfig.license = licenseConfig
|
||||
} catch (licenseError) {
|
||||
console.warn('Failed to load license info:', licenseError)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load additional config info:', error)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load configuration:', error)
|
||||
logger.error('Failed to load configuration:', error)
|
||||
|
||||
// 如果配置加载失败,重定向到登录页而不是抛出错误
|
||||
if (process.client) {
|
||||
@@ -64,12 +32,12 @@ export default defineNuxtPlugin({
|
||||
finalConfig = useRuntimeConfig().public
|
||||
}
|
||||
|
||||
if (!finalConfig.api.baseURL) {
|
||||
// 如果仍然没有 baseURL,重定向到登录页
|
||||
if (!finalConfig.api?.baseURL && !finalConfig.serverHost) {
|
||||
// 如果仍然没有 baseURL 或 serverHost,重定向到登录页
|
||||
if (process.client) {
|
||||
const currentRoute = useRoute()
|
||||
if (currentRoute.path !== '/auth/login') {
|
||||
await navigateTo('/auth/login?showConfig=1')
|
||||
await navigateTo('/auth/login')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-37
@@ -1,87 +1,99 @@
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
import type { SiteConfig } from "~/types/config";
|
||||
import type { DeserializeHandler, DeserializeHandlerArguments, DeserializeHandlerOutput } from "@aws-sdk/types";
|
||||
import { S3Client } from '@aws-sdk/client-s3'
|
||||
import type { SiteConfig } from '~/types/config'
|
||||
import type {
|
||||
DeserializeHandler,
|
||||
DeserializeHandlerArguments,
|
||||
DeserializeHandlerOutput
|
||||
} from '@aws-sdk/types'
|
||||
|
||||
interface S3Response {
|
||||
response?: {
|
||||
body?: string;
|
||||
};
|
||||
[key: string]: any;
|
||||
body?: string
|
||||
}
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: "s3-client",
|
||||
name: 's3-client',
|
||||
setup(nuxtApp) {
|
||||
const { credentials, isAuthenticated } = useAuth();
|
||||
const siteConfig = nuxtApp.$siteConfig as SiteConfig;
|
||||
const { credentials, isAuthenticated } = useAuth()
|
||||
const siteConfig = nuxtApp.$siteConfig as SiteConfig
|
||||
|
||||
if (!isAuthenticated || !credentials.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const client = new S3Client({
|
||||
endpoint: siteConfig.s3.endpoint,
|
||||
region: siteConfig.s3.region || "us-east-1",
|
||||
region: siteConfig.s3.region || 'us-east-1',
|
||||
forcePathStyle: true,
|
||||
// https://github.com/aws/aws-sdk-js-v3/issues/6834#issuecomment-2611346849
|
||||
requestChecksumCalculation: "WHEN_REQUIRED",
|
||||
requestChecksumCalculation: 'WHEN_REQUIRED',
|
||||
credentials: {
|
||||
accessKeyId: credentials.value?.AccessKeyId || "",
|
||||
secretAccessKey: credentials.value?.SecretAccessKey || "",
|
||||
sessionToken: credentials.value?.SessionToken || "",
|
||||
},
|
||||
});
|
||||
accessKeyId: credentials.value?.AccessKeyId || '',
|
||||
secretAccessKey: credentials.value?.SecretAccessKey || '',
|
||||
sessionToken: credentials.value?.SessionToken || ''
|
||||
}
|
||||
})
|
||||
|
||||
// 添加中间件处理XML响应和401错误
|
||||
client.middlewareStack.add(
|
||||
(next: DeserializeHandler<any, any>) =>
|
||||
async (args: DeserializeHandlerArguments<any>): Promise<DeserializeHandlerOutput<any>> => {
|
||||
async (
|
||||
args: DeserializeHandlerArguments<any>
|
||||
): Promise<DeserializeHandlerOutput<any>> => {
|
||||
try {
|
||||
const response = (await next(args)) as S3Response;
|
||||
const response = (await next(args)) as S3Response
|
||||
|
||||
// 检查响应是否为XML格式
|
||||
if (response.response?.body && typeof response.response.body === "string") {
|
||||
const body = response.response.body.trim();
|
||||
if (
|
||||
response.response?.body &&
|
||||
typeof response.response.body === 'string'
|
||||
) {
|
||||
const body = response.response.body.trim()
|
||||
|
||||
// 检查是否是空的XML响应
|
||||
if (body.match(/^<\?xml[^>]*\?><[^>]*><\/[^>]*>$/)) {
|
||||
// 从XML标签名提取属性名
|
||||
const tagName = body.match(/<([^>]*)><\/\1>/)?.[1];
|
||||
const tagName = body.match(/<([^>]*)><\/\1>/)?.[1]
|
||||
if (tagName) {
|
||||
// 将XML标签名转换为驼峰命名
|
||||
const propertyName = tagName.replace(/(?:^|_)([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
const propertyName = tagName.replace(
|
||||
/(?:^|_)([a-z])/g,
|
||||
(_, letter) => letter.toUpperCase()
|
||||
)
|
||||
// 返回null,保持response结构
|
||||
return {
|
||||
response: response.response,
|
||||
[propertyName]: null,
|
||||
} as unknown as DeserializeHandlerOutput<any>;
|
||||
[propertyName]: null
|
||||
} as unknown as DeserializeHandlerOutput<any>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response as DeserializeHandlerOutput<any>;
|
||||
return response as DeserializeHandlerOutput<any>
|
||||
} catch (error: any) {
|
||||
if (error?.$metadata?.httpStatusCode === 401) {
|
||||
await useAuth().logoutAndRedirect();
|
||||
await useAuth().logoutAndRedirect()
|
||||
}
|
||||
|
||||
// 处理 S3 客户端错误,优先抛出 error.message 作为新的 Error
|
||||
if (error?.Code) {
|
||||
throw new Error(error.Code);
|
||||
throw new Error(error.Code)
|
||||
}
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
},
|
||||
{
|
||||
step: "deserialize",
|
||||
name: "handleXmlResponse",
|
||||
step: 'deserialize',
|
||||
name: 'handleXmlResponse'
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
return {
|
||||
provide: {
|
||||
s3Client: client,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
s3Client: client
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Generated
+13189
File diff suppressed because it is too large
Load Diff
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2024 Beijing Henghesha Technology Co., Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"api": {
|
||||
"baseURL": "<PROTOCOL>://<HOST>:<PORT>/rustfs/admin/v3"
|
||||
},
|
||||
"s3": {
|
||||
"endpoint": "<PROTOCOL>://<HOST>:<PORT>",
|
||||
"region": "<REGION>"
|
||||
},
|
||||
"release": {
|
||||
"version": "<RELEASE_VERSION>",
|
||||
"date": "<RELEASE_DATE>"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache-2.0",
|
||||
"expired": "1744017568"
|
||||
}
|
||||
}
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
export interface NavItem {
|
||||
label: string
|
||||
to?: string
|
||||
type?: string
|
||||
icon?: string
|
||||
target?: string
|
||||
key?: string
|
||||
children?: NavItem[]
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
navs: NavItem[]
|
||||
name: string
|
||||
description: string
|
||||
[key: string]: any
|
||||
}
|
||||
Vendored
+1
-12
@@ -9,24 +9,13 @@ export type ApiConfig = {
|
||||
baseURL: string;
|
||||
}
|
||||
|
||||
export type ReleaseConfig = {
|
||||
version: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export type LicenseConfig = {
|
||||
name: string;
|
||||
expired: number;
|
||||
}
|
||||
|
||||
export type SessionConfig = {
|
||||
durationSeconds: number;
|
||||
}
|
||||
|
||||
export interface SiteConfig {
|
||||
serverHost: string;
|
||||
api: ApiConfig;
|
||||
s3: S3Config;
|
||||
release: ReleaseConfig;
|
||||
license: LicenseConfig ;
|
||||
session?: SessionConfig;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { SiteConfig } from '~/types/config'
|
||||
import { logger } from './logger'
|
||||
|
||||
/**
|
||||
* 创建默认配置
|
||||
*/
|
||||
export const createDefaultConfig = (serverHost: string): SiteConfig => {
|
||||
return {
|
||||
serverHost,
|
||||
api: {
|
||||
baseURL: `${serverHost}/rustfs/admin/v3`
|
||||
},
|
||||
s3: {
|
||||
endpoint: serverHost,
|
||||
region: 'us-east-1',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从localStorage获取保存的主机配置
|
||||
*/
|
||||
export const getStoredHostConfig = (): SiteConfig | null => {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
const savedHost = localStorage.getItem('rustfs-server-host')
|
||||
if (!savedHost) return null
|
||||
|
||||
try {
|
||||
const url = new URL(savedHost)
|
||||
const serverHost = `${url.protocol}//${url.host}`
|
||||
return createDefaultConfig(serverHost)
|
||||
} catch (error) {
|
||||
logger.warn('Invalid saved host configuration:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前浏览器主机配置
|
||||
*/
|
||||
export const getCurrentBrowserConfig = (): SiteConfig | null => {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
const currentHost = window.location.host
|
||||
const protocol = window.location.protocol.replace(':', '')
|
||||
const serverHost = `${protocol}://${currentHost}`
|
||||
|
||||
return createDefaultConfig(serverHost)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务端默认配置
|
||||
*/
|
||||
export const getServerDefaultConfig = (): SiteConfig => {
|
||||
// 注意:这里按照用户要求,服务端也应该尽量使用当前host
|
||||
// 但由于服务端限制,只能使用localhost作为fallback
|
||||
const defaultServerHost = 'http://localhost:9000'
|
||||
return createDefaultConfig(defaultServerHost)
|
||||
}
|
||||
+74
-106
@@ -1,11 +1,15 @@
|
||||
export interface ServerConfig {
|
||||
protocol: string
|
||||
host: string
|
||||
port: string
|
||||
region: string
|
||||
}
|
||||
import type { SiteConfig } from '~/types/config'
|
||||
import { handleConfigError } from './error-handler'
|
||||
import { logger } from './logger'
|
||||
import {
|
||||
createDefaultConfig,
|
||||
getStoredHostConfig,
|
||||
getCurrentBrowserConfig,
|
||||
getServerDefaultConfig
|
||||
} from './config-helpers'
|
||||
|
||||
export interface RustFSConfig {
|
||||
serverHost: string
|
||||
api: {
|
||||
baseURL: string
|
||||
}
|
||||
@@ -15,137 +19,101 @@ export interface RustFSConfig {
|
||||
}
|
||||
}
|
||||
|
||||
const CONFIG_KEY = 'rustfs-config'
|
||||
// 添加配置缓存
|
||||
let configCache: SiteConfig | null = null
|
||||
let configCacheTime = 0
|
||||
const CACHE_DURATION = 60000 // 1分钟缓存
|
||||
|
||||
export const configManager = {
|
||||
// 从 public/config.json 读取配置
|
||||
async loadPublicConfig(): Promise<RustFSConfig | null> {
|
||||
if (process.client) {
|
||||
try {
|
||||
const response = await fetch('/config.json')
|
||||
if (response.ok) {
|
||||
const publicConfig = await response.json()
|
||||
if (publicConfig.api?.baseURL) {
|
||||
return publicConfig
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('public/config.json not found')
|
||||
}
|
||||
// 获取当前host配置
|
||||
getCurrentHostConfig(): SiteConfig {
|
||||
// 优先使用localStorage中保存的配置
|
||||
const storedConfig = getStoredHostConfig()
|
||||
if (storedConfig) {
|
||||
return storedConfig
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
// 从 localStorage 读取配置
|
||||
loadStorageConfig(): RustFSConfig | null {
|
||||
if (process.client) {
|
||||
try {
|
||||
const configStr = localStorage.getItem(CONFIG_KEY)
|
||||
if (configStr) {
|
||||
return JSON.parse(configStr)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse localStorage config:', error)
|
||||
}
|
||||
// 使用当前浏览器地址
|
||||
const browserConfig = getCurrentBrowserConfig()
|
||||
if (browserConfig) {
|
||||
return browserConfig
|
||||
}
|
||||
return null
|
||||
|
||||
// 服务端fallback
|
||||
return getServerDefaultConfig()
|
||||
},
|
||||
|
||||
// 从 nuxt runtimeconfig 读取配置
|
||||
loadRuntimeConfig(): RustFSConfig | null {
|
||||
loadRuntimeConfig(): SiteConfig | null {
|
||||
try {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
if (runtimeConfig.public?.api?.baseURL) {
|
||||
|
||||
// 优先使用 serverHost,然后是 API_BASE_URL
|
||||
const serverHost = runtimeConfig.public?.serverHost ||
|
||||
runtimeConfig.public?.api?.baseURL?.replace(/\/rustfs\/admin\/v3$/, '')
|
||||
|
||||
if (serverHost) {
|
||||
return {
|
||||
serverHost,
|
||||
api: {
|
||||
baseURL: runtimeConfig.public.api.baseURL
|
||||
baseURL: runtimeConfig.public.api?.baseURL || `${serverHost}/rustfs/admin/v3`
|
||||
},
|
||||
s3: {
|
||||
endpoint: runtimeConfig.public.s3?.endpoint || runtimeConfig.public.api.baseURL,
|
||||
region: runtimeConfig.public.s3?.region || 'us-east-1'
|
||||
}
|
||||
endpoint: runtimeConfig.public.s3?.endpoint || serverHost,
|
||||
region: runtimeConfig.public.s3?.region || 'us-east-1',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: ''
|
||||
},
|
||||
session: runtimeConfig.public.session
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load runtime config:', error)
|
||||
const configError = handleConfigError(error, 'runtime config loading')
|
||||
logger.warn('Failed to load runtime config:', configError.message)
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
// 按优先级加载配置:config.json > localStorage > runtimeconfig
|
||||
async loadConfig(): Promise<RustFSConfig | null> {
|
||||
// 首先尝试从 config.json 加载
|
||||
const publicConfig = await this.loadPublicConfig()
|
||||
if (publicConfig) {
|
||||
return publicConfig
|
||||
// 加载配置:优先使用localStorage,然后是当前host,最后是runtimeconfig
|
||||
async loadConfig(): Promise<SiteConfig> {
|
||||
// 检查缓存
|
||||
const now = Date.now()
|
||||
if (configCache && (now - configCacheTime) < CACHE_DURATION) {
|
||||
return configCache
|
||||
}
|
||||
|
||||
// 如果没有 config.json,尝试从 localStorage 加载
|
||||
const storageConfig = this.loadStorageConfig()
|
||||
if (storageConfig) {
|
||||
return storageConfig
|
||||
}
|
||||
let config: SiteConfig
|
||||
|
||||
// 如果都没有,使用 runtimeconfig 作为默认值
|
||||
return this.loadRuntimeConfig()
|
||||
},
|
||||
|
||||
// 从 RustFSConfig 提取 ServerConfig
|
||||
extractServerConfig(config: RustFSConfig): ServerConfig | null {
|
||||
try {
|
||||
const urlObj = new URL(config.api.baseURL)
|
||||
return {
|
||||
protocol: urlObj.protocol.replace(':', ''),
|
||||
host: urlObj.hostname,
|
||||
port: urlObj.port || (urlObj.protocol === 'https:' ? '443' : '80'),
|
||||
region: config.s3.region
|
||||
// 优先使用当前host配置(包括localStorage检查)
|
||||
const currentHostConfig = this.getCurrentHostConfig()
|
||||
if (currentHostConfig) {
|
||||
config = currentHostConfig
|
||||
} else {
|
||||
// 如果没有当前host配置,使用 runtimeconfig
|
||||
const runtimeConfig = this.loadRuntimeConfig()
|
||||
if (runtimeConfig) {
|
||||
config = runtimeConfig
|
||||
} else {
|
||||
// 最后使用浏览器当前地址或服务端默认值
|
||||
config = getCurrentBrowserConfig() || getServerDefaultConfig()
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to extract server config:', error)
|
||||
return null
|
||||
}
|
||||
|
||||
// 缓存配置
|
||||
configCache = config
|
||||
configCacheTime = now
|
||||
return config
|
||||
},
|
||||
|
||||
// 检查是否有 public/config.json
|
||||
async hasPublicConfig(): Promise<boolean> {
|
||||
const config = await this.loadPublicConfig()
|
||||
return !!config
|
||||
},
|
||||
|
||||
// 保存配置到 localStorage(只有当没有 public/config.json 时)
|
||||
async saveConfig(serverConfig: ServerConfig): Promise<boolean> {
|
||||
if (process.client) {
|
||||
// 如果有 public/config.json,就不保存到 localStorage
|
||||
if (await this.hasPublicConfig()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const config: RustFSConfig = {
|
||||
api: {
|
||||
baseURL: `${serverConfig.protocol}://${serverConfig.host}:${serverConfig.port}/rustfs/admin/v3`
|
||||
},
|
||||
s3: {
|
||||
endpoint: `${serverConfig.protocol}://${serverConfig.host}:${serverConfig.port}`,
|
||||
region: serverConfig.region
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem(CONFIG_KEY, JSON.stringify(config))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
// 清除配置
|
||||
clearConfig(): void {
|
||||
if (process.client) {
|
||||
localStorage.removeItem(CONFIG_KEY)
|
||||
}
|
||||
// 清除缓存
|
||||
clearCache() {
|
||||
configCache = null
|
||||
configCacheTime = 0
|
||||
},
|
||||
|
||||
// 检查是否有有效配置
|
||||
async hasValidConfig(): Promise<boolean> {
|
||||
const config = await this.loadConfig()
|
||||
return !!(config?.api?.baseURL)
|
||||
return !!(config?.serverHost && config?.api?.baseURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
export interface ApiError {
|
||||
message: string
|
||||
code?: string
|
||||
statusCode?: number
|
||||
originalError?: Error
|
||||
}
|
||||
|
||||
export class ConfigLoadError extends Error {
|
||||
code: 'INVALID_URL' | 'STORAGE_ERROR' | 'NETWORK_ERROR' | 'UNKNOWN_ERROR'
|
||||
originalError?: Error
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
code: ConfigLoadError['code'] = 'UNKNOWN_ERROR',
|
||||
originalError?: Error
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'ConfigLoadError'
|
||||
this.code = code
|
||||
this.originalError = originalError
|
||||
}
|
||||
}
|
||||
|
||||
export const parseApiError = async (response: Response): Promise<string> => {
|
||||
try {
|
||||
// 优先尝试解析为 JSON
|
||||
const errorData = await response.clone().json()
|
||||
return errorData.message || JSON.stringify(errorData) || response.statusText
|
||||
} catch (e) {
|
||||
try {
|
||||
// 如果不是 JSON,尝试解析为文本
|
||||
const text = await response.clone().text()
|
||||
if (text) {
|
||||
// 检查是否为 XML
|
||||
if (text.trim().startsWith('<')) {
|
||||
// 简单提取 <Message> 或 <Error> 标签内容
|
||||
const match = text.match(/<Message>(.*?)<\/Message>/i) ||
|
||||
text.match(/<Error>(.*?)<\/Error>/i)
|
||||
return match ? match[1] : text
|
||||
}
|
||||
return text
|
||||
}
|
||||
} catch (e2) {
|
||||
// 保持原有 statusText
|
||||
}
|
||||
}
|
||||
return response.statusText
|
||||
}
|
||||
|
||||
export const handleConfigError = (error: unknown, context: string): ConfigLoadError => {
|
||||
if (error instanceof ConfigLoadError) {
|
||||
return error
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('Invalid URL')) {
|
||||
return new ConfigLoadError(
|
||||
`Invalid URL in ${context}: ${error.message}`,
|
||||
'INVALID_URL',
|
||||
error
|
||||
)
|
||||
}
|
||||
if (error.message.includes('localStorage') || error.message.includes('storage')) {
|
||||
return new ConfigLoadError(
|
||||
`Storage error in ${context}: ${error.message}`,
|
||||
'STORAGE_ERROR',
|
||||
error
|
||||
)
|
||||
}
|
||||
if (error.message.includes('fetch') || error.message.includes('network')) {
|
||||
return new ConfigLoadError(
|
||||
`Network error in ${context}: ${error.message}`,
|
||||
'NETWORK_ERROR',
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return new ConfigLoadError(
|
||||
`Unknown error in ${context}: ${error}`,
|
||||
'UNKNOWN_ERROR',
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
|
||||
export const logger = {
|
||||
log: (...args: any[]) => {
|
||||
if (isDevelopment) {
|
||||
console.log(...args)
|
||||
}
|
||||
},
|
||||
warn: (...args: any[]) => {
|
||||
if (isDevelopment) {
|
||||
console.warn(...args)
|
||||
}
|
||||
},
|
||||
error: (...args: any[]) => {
|
||||
if (isDevelopment) {
|
||||
console.error(...args)
|
||||
}
|
||||
},
|
||||
info: (...args: any[]) => {
|
||||
if (isDevelopment) {
|
||||
console.info(...args)
|
||||
}
|
||||
},
|
||||
debug: (...args: any[]) => {
|
||||
if (isDevelopment) {
|
||||
console.debug(...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user