feat(auth): add SMS OTP verification for phone registration and login
- auth-service: add SmsService (Aliyun SMS) + RedisProvider for OTP storage - POST /api/v1/auth/sms/send — send OTP (rate limited 1/min per phone) - POST /api/v1/auth/sms/verify — verify OTP only - POST /api/v1/auth/login/otp — passwordless login with phone + OTP - register endpoint now requires smsCode when registering with phone - Web Admin register page: add OTP input + 60s countdown button for phone mode - Flutter login page: add 验证码登录 tab with phone + OTP flow - SMS enabled via ALIYUN_ACCESS_KEY_ID/SECRET + SMS_ENABLED=true env vars - Falls back to mock mode (logs code) when env vars not set Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2773b6265c
commit
71ea80972d
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
@ -23,6 +23,10 @@ export default function RegisterPage() {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
|
const [smsCode, setSmsCode] = useState('');
|
||||||
|
const [smsCooldown, setSmsCooldown] = useState(0);
|
||||||
|
const [smsSending, setSmsSending] = useState(false);
|
||||||
|
const cooldownRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const [companyName, setCompanyName] = useState('');
|
const [companyName, setCompanyName] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
|
@ -40,6 +44,27 @@ export default function RegisterPage() {
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleSendSms = async () => {
|
||||||
|
if (!phone.trim()) { setError('请先填写手机号'); return; }
|
||||||
|
setError(null);
|
||||||
|
setSmsSending(true);
|
||||||
|
try {
|
||||||
|
await apiClient('/api/v1/auth/sms/send', { method: 'POST', body: { phone: phone.trim(), purpose: 'register' } });
|
||||||
|
setSmsCooldown(60);
|
||||||
|
if (cooldownRef.current) clearInterval(cooldownRef.current);
|
||||||
|
cooldownRef.current = setInterval(() => {
|
||||||
|
setSmsCooldown((prev) => {
|
||||||
|
if (prev <= 1) { clearInterval(cooldownRef.current!); return 0; }
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : '发送失败,请重试');
|
||||||
|
} finally {
|
||||||
|
setSmsSending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) {
|
||||||
|
|
@ -67,7 +92,9 @@ export default function RegisterPage() {
|
||||||
if (loginMethod === 'email') {
|
if (loginMethod === 'email') {
|
||||||
body.email = email;
|
body.email = email;
|
||||||
} else {
|
} else {
|
||||||
|
if (!smsCode.trim()) { setError('请输入短信验证码'); setIsLoading(false); return; }
|
||||||
body.phone = phone;
|
body.phone = phone;
|
||||||
|
body.smsCode = smsCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await apiClient<RegisterResponse>('/api/v1/auth/register', {
|
const data = await apiClient<RegisterResponse>('/api/v1/auth/register', {
|
||||||
|
|
@ -149,6 +176,7 @@ export default function RegisterPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">{t('phone')}</label>
|
<label className="block text-sm font-medium mb-1">{t('phone')}</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -160,6 +188,29 @@ export default function RegisterPage() {
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">短信验证码</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={smsCode}
|
||||||
|
onChange={(e) => setSmsCode(e.target.value)}
|
||||||
|
className="flex-1 px-3 py-2 bg-input border rounded-md text-sm"
|
||||||
|
placeholder="请输入 6 位验证码"
|
||||||
|
maxLength={6}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSendSms}
|
||||||
|
disabled={smsSending || smsCooldown > 0}
|
||||||
|
className="px-4 py-2 text-sm font-medium bg-secondary text-secondary-foreground rounded-md hover:opacity-90 disabled:opacity-50 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{smsSending ? '发送中...' : smsCooldown > 0 ? `${smsCooldown}s` : '获取验证码'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Full name */}
|
{/* Full name */}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,12 @@ class ApiEndpoints {
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
static const String login = '$auth/login';
|
static const String login = '$auth/login';
|
||||||
|
static const String loginOtp = '$auth/login/otp';
|
||||||
static const String register = '$auth/register';
|
static const String register = '$auth/register';
|
||||||
static const String profile = '$auth/profile';
|
static const String profile = '$auth/profile';
|
||||||
static const String refreshToken = '$auth/refresh';
|
static const String refreshToken = '$auth/refresh';
|
||||||
|
static const String smsSend = '$auth/sms/send';
|
||||||
|
static const String smsVerify = '$auth/sms/verify';
|
||||||
|
|
||||||
// Agent
|
// Agent
|
||||||
static const String tasks = '$agent/tasks';
|
static const String tasks = '$agent/tasks';
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,44 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send SMS OTP to the given phone number.
|
||||||
|
Future<void> sendSmsCode(String phone) async {
|
||||||
|
final config = _ref.read(appConfigProvider);
|
||||||
|
final dio = Dio(BaseOptions(baseUrl: config.apiBaseUrl));
|
||||||
|
await dio.post(ApiEndpoints.smsSend, data: {'phone': phone, 'purpose': 'login'});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Login with phone number + OTP (passwordless).
|
||||||
|
Future<bool> loginWithOtp(String phone, String smsCode) async {
|
||||||
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
try {
|
||||||
|
final config = _ref.read(appConfigProvider);
|
||||||
|
final dio = Dio(BaseOptions(baseUrl: config.apiBaseUrl));
|
||||||
|
final response = await dio.post(
|
||||||
|
ApiEndpoints.loginOtp,
|
||||||
|
data: {'phone': phone, 'smsCode': smsCode},
|
||||||
|
);
|
||||||
|
final authResponse = AuthResponse.fromJson(response.data as Map<String, dynamic>);
|
||||||
|
final storage = _ref.read(secureStorageProvider);
|
||||||
|
await storage.write(key: _keyAccessToken, value: authResponse.accessToken);
|
||||||
|
await storage.write(key: _keyRefreshToken, value: authResponse.refreshToken);
|
||||||
|
state = state.copyWith(isAuthenticated: true, isLoading: false, user: authResponse.user);
|
||||||
|
_ref.invalidate(accessTokenProvider);
|
||||||
|
if (authResponse.user.tenantId != null) {
|
||||||
|
_ref.read(currentTenantIdProvider.notifier).state = authResponse.user.tenantId;
|
||||||
|
}
|
||||||
|
_connectNotifications(authResponse.user.tenantId);
|
||||||
|
return true;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
final message = (e.response?.data is Map) ? e.response?.data['message'] : null;
|
||||||
|
state = state.copyWith(isLoading: false, error: message?.toString() ?? '验证码登录失败');
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
state = state.copyWith(isLoading: false, error: ErrorHandler.friendlyMessage(e));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> login(String email, String password) async {
|
Future<bool> login(String email, String password) async {
|
||||||
state = state.copyWith(isLoading: true, error: null);
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
@ -5,6 +6,8 @@ import 'package:go_router/go_router.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../../data/providers/auth_provider.dart';
|
import '../../data/providers/auth_provider.dart';
|
||||||
|
|
||||||
|
enum _LoginMode { password, otp }
|
||||||
|
|
||||||
class LoginPage extends ConsumerStatefulWidget {
|
class LoginPage extends ConsumerStatefulWidget {
|
||||||
const LoginPage({super.key});
|
const LoginPage({super.key});
|
||||||
|
|
||||||
|
|
@ -16,6 +19,56 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController();
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
|
final _phoneController = TextEditingController();
|
||||||
|
final _otpController = TextEditingController();
|
||||||
|
|
||||||
|
_LoginMode _mode = _LoginMode.password;
|
||||||
|
int _smsCooldown = 0;
|
||||||
|
bool _smsSending = false;
|
||||||
|
Timer? _cooldownTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_emailController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_phoneController.dispose();
|
||||||
|
_otpController.dispose();
|
||||||
|
_cooldownTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendSmsCode() async {
|
||||||
|
final phone = _phoneController.text.trim();
|
||||||
|
if (phone.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('请先输入手机号')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _smsSending = true);
|
||||||
|
try {
|
||||||
|
await ref.read(authStateProvider.notifier).sendSmsCode(phone);
|
||||||
|
setState(() => _smsCooldown = 60);
|
||||||
|
_cooldownTimer = Timer.periodic(const Duration(seconds: 1), (t) {
|
||||||
|
setState(() {
|
||||||
|
if (_smsCooldown <= 1) {
|
||||||
|
_smsCooldown = 0;
|
||||||
|
t.cancel();
|
||||||
|
} else {
|
||||||
|
_smsCooldown--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(e.toString())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _smsSending = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -33,11 +86,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
SvgPicture.asset('assets/icons/logo.svg', width: 96, height: 96),
|
||||||
'assets/icons/logo.svg',
|
|
||||||
width: 96,
|
|
||||||
height: 96,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
const Text(
|
||||||
'iAgent',
|
'iAgent',
|
||||||
|
|
@ -51,12 +100,27 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
const Text(
|
const Text(
|
||||||
'服务器集群运维智能体',
|
'服务器集群运维智能体',
|
||||||
style: TextStyle(
|
style: TextStyle(color: AppColors.textSecondary, fontSize: 14),
|
||||||
color: AppColors.textSecondary,
|
),
|
||||||
fontSize: 14,
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Login mode toggle
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.surface,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: AppColors.surfaceLight),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _modeButton('密码登录', _LoginMode.password)),
|
||||||
|
Expanded(child: _modeButton('验证码登录', _LoginMode.otp)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
if (_mode == _LoginMode.password) ...[
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
|
@ -65,13 +129,9 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
hintText: 'user@example.com',
|
hintText: 'user@example.com',
|
||||||
prefixIcon: Icon(Icons.email_outlined),
|
prefixIcon: Icon(Icons.email_outlined),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (v) {
|
||||||
if (value == null || value.isEmpty) {
|
if (v == null || v.isEmpty) return '请输入邮箱地址';
|
||||||
return '请输入邮箱地址';
|
if (!v.contains('@')) return '请输入有效的邮箱地址';
|
||||||
}
|
|
||||||
if (!value.contains('@')) {
|
|
||||||
return '请输入有效的邮箱地址';
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -83,37 +143,74 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
labelText: '密码',
|
labelText: '密码',
|
||||||
prefixIcon: Icon(Icons.lock_outline),
|
prefixIcon: Icon(Icons.lock_outline),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (v) => (v == null || v.isEmpty) ? '请输入密码' : null,
|
||||||
if (value == null || value.isEmpty) {
|
onFieldSubmitted: (_) => _handlePasswordLogin(),
|
||||||
return '请输入密码';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onFieldSubmitted: (_) => _handleLogin(),
|
|
||||||
),
|
),
|
||||||
|
] else ...[
|
||||||
|
TextFormField(
|
||||||
|
controller: _phoneController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: '手机号',
|
||||||
|
hintText: '+86 138 0000 0000',
|
||||||
|
prefixIcon: Icon(Icons.phone_outlined),
|
||||||
|
),
|
||||||
|
validator: (v) => (v == null || v.isEmpty) ? '请输入手机号' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _otpController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 6,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: '验证码',
|
||||||
|
hintText: '6 位数字',
|
||||||
|
prefixIcon: Icon(Icons.sms_outlined),
|
||||||
|
counterText: '',
|
||||||
|
),
|
||||||
|
validator: (v) => (v == null || v.isEmpty) ? '请输入验证码' : null,
|
||||||
|
onFieldSubmitted: (_) => _handleOtpLogin(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 56,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: (_smsSending || _smsCooldown > 0) ? null : _sendSmsCode,
|
||||||
|
child: Text(
|
||||||
|
_smsSending
|
||||||
|
? '发送中'
|
||||||
|
: _smsCooldown > 0
|
||||||
|
? '${_smsCooldown}s'
|
||||||
|
: '获取验证码',
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
if (authState.error != null) ...[
|
if (authState.error != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
horizontal: 12,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.error.withAlpha(25),
|
color: AppColors.error.withAlpha(25),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.error_outline,
|
const Icon(Icons.error_outline, color: AppColors.error, size: 18),
|
||||||
color: AppColors.error, size: 18),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
authState.error!,
|
authState.error!,
|
||||||
style: const TextStyle(
|
style: const TextStyle(color: AppColors.error, fontSize: 13),
|
||||||
color: AppColors.error,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -125,29 +222,25 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 48,
|
height: 48,
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
onPressed: authState.isLoading ? null : _handleLogin,
|
onPressed: authState.isLoading
|
||||||
|
? null
|
||||||
|
: (_mode == _LoginMode.password
|
||||||
|
? _handlePasswordLogin
|
||||||
|
: _handleOtpLogin),
|
||||||
child: authState.isLoading
|
child: authState.isLoading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2, color: Colors.white),
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: const Text(
|
: const Text('登录', style: TextStyle(fontSize: 16)),
|
||||||
'登录',
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Text(
|
Text(
|
||||||
'账号由管理员在后台创建或通过邀请链接注册',
|
'账号由管理员在后台创建或通过邀请链接注册',
|
||||||
style: TextStyle(
|
style: TextStyle(color: AppColors.textMuted, fontSize: 12),
|
||||||
color: AppColors.textMuted,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -159,23 +252,44 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleLogin() async {
|
Widget _modeButton(String label, _LoginMode mode) {
|
||||||
if (!_formKey.currentState!.validate()) return;
|
final selected = _mode == mode;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => _mode = mode),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected ? AppColors.primary : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(7),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: selected ? Colors.white : AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handlePasswordLogin() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
final success = await ref.read(authStateProvider.notifier).login(
|
final success = await ref.read(authStateProvider.notifier).login(
|
||||||
_emailController.text.trim(),
|
_emailController.text.trim(),
|
||||||
_passwordController.text,
|
_passwordController.text,
|
||||||
);
|
);
|
||||||
|
if (success && mounted) context.go('/dashboard');
|
||||||
if (success && mounted) {
|
|
||||||
context.go('/dashboard');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _handleOtpLogin() async {
|
||||||
void dispose() {
|
if (!_formKey.currentState!.validate()) return;
|
||||||
_emailController.dispose();
|
final success = await ref.read(authStateProvider.notifier).loginWithOtp(
|
||||||
_passwordController.dispose();
|
_phoneController.text.trim(),
|
||||||
super.dispose();
|
_otpController.text.trim(),
|
||||||
|
);
|
||||||
|
if (success && mounted) context.go('/dashboard');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,11 @@
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"@it0/common": "workspace:*",
|
"@it0/common": "workspace:*",
|
||||||
"@it0/database": "workspace:*"
|
"@it0/database": "workspace:*",
|
||||||
|
"@alicloud/dysmsapi20170525": "^4.3.1",
|
||||||
|
"@alicloud/openapi-client": "^0.4.15",
|
||||||
|
"@alicloud/tea-util": "^1.4.11",
|
||||||
|
"ioredis": "^5.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.3.0",
|
"@nestjs/cli": "^10.3.0",
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,30 @@ export class AuthService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login by phone number only (OTP already verified by controller).
|
||||||
|
* The user must exist in public.users with a matching phone.
|
||||||
|
*/
|
||||||
|
async loginByPhone(phone: string): Promise<{
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
user: { id: string; email?: string; phone?: string; name: string; roles: string[]; tenantId: string };
|
||||||
|
}> {
|
||||||
|
const rows = await this.dataSource.query(
|
||||||
|
`SELECT * FROM public.users WHERE phone = $1 AND is_active = true LIMIT 1`, [phone],
|
||||||
|
);
|
||||||
|
if (!rows.length) throw new UnauthorizedException('该手机号未注册');
|
||||||
|
const user = this.mapRow(rows[0]);
|
||||||
|
await this.dataSource.query(
|
||||||
|
`UPDATE public.users SET last_login_at = NOW() WHERE id = $1`, [user.id],
|
||||||
|
);
|
||||||
|
const tokens = this.generateTokens(user);
|
||||||
|
return {
|
||||||
|
...tokens,
|
||||||
|
user: { id: user.id, email: user.email, phone: user.phone, name: user.name, roles: user.roles, tenantId: user.tenantId },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user.
|
* Register a new user.
|
||||||
* If companyName is provided, creates a new tenant with schema provisioning
|
* If companyName is provided, creates a new tenant with schema provisioning
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,14 @@ import { UserController } from './interfaces/rest/controllers/user.controller';
|
||||||
import { SettingsController } from './interfaces/rest/controllers/settings.controller';
|
import { SettingsController } from './interfaces/rest/controllers/settings.controller';
|
||||||
import { RoleController } from './interfaces/rest/controllers/role.controller';
|
import { RoleController } from './interfaces/rest/controllers/role.controller';
|
||||||
import { PermissionController } from './interfaces/rest/controllers/permission.controller';
|
import { PermissionController } from './interfaces/rest/controllers/permission.controller';
|
||||||
|
import { SmsController } from './interfaces/rest/controllers/sms.controller';
|
||||||
import { JwtStrategy } from './infrastructure/strategies/jwt.strategy';
|
import { JwtStrategy } from './infrastructure/strategies/jwt.strategy';
|
||||||
import { RbacGuard } from './infrastructure/guards/rbac.guard';
|
import { RbacGuard } from './infrastructure/guards/rbac.guard';
|
||||||
import { AuthService } from './application/services/auth.service';
|
import { AuthService } from './application/services/auth.service';
|
||||||
import { UserRepository } from './infrastructure/repositories/user.repository';
|
import { UserRepository } from './infrastructure/repositories/user.repository';
|
||||||
import { ApiKeyRepository } from './infrastructure/repositories/api-key.repository';
|
import { ApiKeyRepository } from './infrastructure/repositories/api-key.repository';
|
||||||
|
import { SmsService } from './infrastructure/sms/sms.service';
|
||||||
|
import { RedisProvider } from './infrastructure/redis/redis.provider';
|
||||||
import { User } from './domain/entities/user.entity';
|
import { User } from './domain/entities/user.entity';
|
||||||
import { Role } from './domain/entities/role.entity';
|
import { Role } from './domain/entities/role.entity';
|
||||||
import { ApiKey } from './domain/entities/api-key.entity';
|
import { ApiKey } from './domain/entities/api-key.entity';
|
||||||
|
|
@ -34,7 +37,7 @@ import { TenantInvite } from './domain/entities/tenant-invite.entity';
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
controllers: [AuthController, TenantController, UserController, SettingsController, RoleController, PermissionController],
|
controllers: [AuthController, TenantController, UserController, SettingsController, RoleController, PermissionController, SmsController],
|
||||||
providers: [
|
providers: [
|
||||||
JwtStrategy,
|
JwtStrategy,
|
||||||
RbacGuard,
|
RbacGuard,
|
||||||
|
|
@ -42,6 +45,8 @@ import { TenantInvite } from './domain/entities/tenant-invite.entity';
|
||||||
UserRepository,
|
UserRepository,
|
||||||
ApiKeyRepository,
|
ApiKeyRepository,
|
||||||
TenantProvisioningService,
|
TenantProvisioningService,
|
||||||
|
SmsService,
|
||||||
|
RedisProvider,
|
||||||
],
|
],
|
||||||
exports: [AuthService],
|
exports: [AuthService],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Provider } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
|
||||||
|
export const REDIS_CLIENT = 'REDIS_CLIENT';
|
||||||
|
|
||||||
|
export const RedisProvider: Provider = {
|
||||||
|
provide: REDIS_CLIENT,
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => {
|
||||||
|
return new Redis({
|
||||||
|
host: config.get('REDIS_HOST', 'redis'),
|
||||||
|
port: config.get<number>('REDIS_PORT', 6379),
|
||||||
|
password: config.get('REDIS_PASSWORD') || undefined,
|
||||||
|
lazyConnect: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SmsService } from './sms.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [SmsService],
|
||||||
|
exports: [SmsService],
|
||||||
|
})
|
||||||
|
export class SmsModule {}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import Dysmsapi20170525, * as $Dysmsapi20170525 from '@alicloud/dysmsapi20170525';
|
||||||
|
import * as $OpenApi from '@alicloud/openapi-client';
|
||||||
|
import * as $Util from '@alicloud/tea-util';
|
||||||
|
|
||||||
|
export interface SmsSendResult {
|
||||||
|
success: boolean;
|
||||||
|
requestId?: string;
|
||||||
|
bizId?: string;
|
||||||
|
code?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SmsService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(SmsService.name);
|
||||||
|
private client: Dysmsapi20170525 | null = null;
|
||||||
|
private readonly signName: string;
|
||||||
|
private readonly templateCode: string;
|
||||||
|
private readonly enabled: boolean;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
this.signName = this.configService.get('ALIYUN_SMS_SIGN_NAME', '');
|
||||||
|
this.templateCode = this.configService.get('ALIYUN_SMS_TEMPLATE_CODE', '');
|
||||||
|
this.enabled = this.configService.get('SMS_ENABLED') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
const accessKeyId = this.configService.get<string>('ALIYUN_ACCESS_KEY_ID');
|
||||||
|
const accessKeySecret = this.configService.get<string>('ALIYUN_ACCESS_KEY_SECRET');
|
||||||
|
|
||||||
|
if (!accessKeyId || !accessKeySecret) {
|
||||||
|
this.logger.warn('阿里云 SMS 配置缺失,短信功能将使用模拟模式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = new $OpenApi.Config({
|
||||||
|
accessKeyId,
|
||||||
|
accessKeySecret,
|
||||||
|
endpoint: 'dysmsapi.aliyuncs.com',
|
||||||
|
});
|
||||||
|
this.client = new Dysmsapi20170525(config);
|
||||||
|
this.logger.log('阿里云 SMS 客户端初始化成功');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('阿里云 SMS 客户端初始化失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendVerificationCode(phoneNumber: string, code: string): Promise<SmsSendResult> {
|
||||||
|
const normalized = this.normalizePhone(phoneNumber);
|
||||||
|
this.logger.log(`[SMS] 发送验证码到 ${this.maskPhone(normalized)}`);
|
||||||
|
|
||||||
|
if (!this.enabled || !this.client) {
|
||||||
|
this.logger.warn(`[SMS] 模拟模式: 验证码 ${code} → ${this.maskPhone(normalized)}`);
|
||||||
|
return { success: true, code: 'OK', message: '模拟发送成功' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const req = new $Dysmsapi20170525.SendSmsRequest({
|
||||||
|
phoneNumbers: normalized,
|
||||||
|
signName: this.signName,
|
||||||
|
templateCode: this.templateCode,
|
||||||
|
templateParam: JSON.stringify({ code }),
|
||||||
|
});
|
||||||
|
const runtime = new $Util.RuntimeOptions({ connectTimeout: 15000, readTimeout: 15000 });
|
||||||
|
const resp = await this.client.sendSmsWithOptions(req, runtime);
|
||||||
|
const body = resp.body;
|
||||||
|
const result: SmsSendResult = {
|
||||||
|
success: body?.code === 'OK',
|
||||||
|
requestId: body?.requestId,
|
||||||
|
bizId: body?.bizId,
|
||||||
|
code: body?.code,
|
||||||
|
message: body?.message,
|
||||||
|
};
|
||||||
|
if (result.success) {
|
||||||
|
this.logger.log(`[SMS] 发送成功: requestId=${result.requestId}`);
|
||||||
|
} else {
|
||||||
|
this.logger.error(`[SMS] 发送失败: code=${result.code}, msg=${result.message}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`[SMS] 发送异常: ${error.message}`);
|
||||||
|
return { success: false, code: error.code || 'UNKNOWN_ERROR', message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizePhone(phone: string): string {
|
||||||
|
let p = phone.trim();
|
||||||
|
if (p.startsWith('+86')) p = p.slice(3);
|
||||||
|
else if (p.startsWith('86') && p.length === 13) p = p.slice(2);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private maskPhone(phone: string): string {
|
||||||
|
if (phone.length < 7) return phone;
|
||||||
|
return phone.slice(0, 3) + '****' + phone.slice(-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
import { Controller, Post, Body, Get, Param, UseGuards, Request } from '@nestjs/common';
|
import { Controller, Post, Body, Get, Param, UseGuards, Request, Inject, BadRequestException } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthService } from '../../../application/services/auth.service';
|
import { AuthService } from '../../../application/services/auth.service';
|
||||||
|
import { REDIS_CLIENT } from '../../../infrastructure/redis/redis.provider';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
|
||||||
@Controller('api/v1/auth')
|
@Controller('api/v1/auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private readonly authService: AuthService) {}
|
constructor(
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
@Inject(REDIS_CLIENT) private readonly redis: Redis,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
async login(
|
async login(
|
||||||
|
|
@ -21,6 +26,7 @@ export class AuthController {
|
||||||
* Register a new enterprise account.
|
* Register a new enterprise account.
|
||||||
* Supports email or phone as the login identifier.
|
* Supports email or phone as the login identifier.
|
||||||
* If companyName is provided, creates a new tenant (self-service registration).
|
* If companyName is provided, creates a new tenant (self-service registration).
|
||||||
|
* Phone registration requires a valid OTP (smsCode).
|
||||||
*/
|
*/
|
||||||
@Post('register')
|
@Post('register')
|
||||||
async register(
|
async register(
|
||||||
|
|
@ -28,11 +34,25 @@ export class AuthController {
|
||||||
body: {
|
body: {
|
||||||
email?: string;
|
email?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
smsCode?: string;
|
||||||
password: string;
|
password: string;
|
||||||
name: string;
|
name: string;
|
||||||
companyName?: string;
|
companyName?: string;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
// Verify OTP when registering with phone
|
||||||
|
if (body.phone) {
|
||||||
|
const phone = body.phone.trim();
|
||||||
|
if (!body.smsCode) throw new BadRequestException('手机注册需要验证码');
|
||||||
|
const otpKey = `sms:otp:${phone}`;
|
||||||
|
const stored = await this.redis.get(otpKey);
|
||||||
|
if (!stored || stored !== body.smsCode.trim()) {
|
||||||
|
throw new BadRequestException('验证码错误或已过期');
|
||||||
|
}
|
||||||
|
// Consume the OTP immediately
|
||||||
|
await this.redis.del(otpKey);
|
||||||
|
}
|
||||||
|
|
||||||
return this.authService.register(
|
return this.authService.register(
|
||||||
body.password,
|
body.password,
|
||||||
body.name,
|
body.name,
|
||||||
|
|
@ -42,6 +62,26 @@ export class AuthController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login with phone number + OTP (passwordless).
|
||||||
|
* POST /api/v1/auth/login/otp
|
||||||
|
*/
|
||||||
|
@Post('login/otp')
|
||||||
|
async loginWithOtp(@Body() body: { phone: string; smsCode: string }) {
|
||||||
|
const phone = (body.phone || '').trim();
|
||||||
|
const code = (body.smsCode || '').trim();
|
||||||
|
if (!phone || !code) throw new BadRequestException('手机号和验证码不能为空');
|
||||||
|
|
||||||
|
const otpKey = `sms:otp:${phone}`;
|
||||||
|
const stored = await this.redis.get(otpKey);
|
||||||
|
if (!stored || stored !== code) {
|
||||||
|
throw new BadRequestException('验证码错误或已过期');
|
||||||
|
}
|
||||||
|
await this.redis.del(otpKey);
|
||||||
|
|
||||||
|
return this.authService.loginByPhone(phone);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate an invitation token (public endpoint).
|
* Validate an invitation token (public endpoint).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Inject,
|
||||||
|
BadRequestException,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { SmsService } from '../../../infrastructure/sms/sms.service';
|
||||||
|
import { REDIS_CLIENT } from '../../../infrastructure/redis/redis.provider';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
|
||||||
|
const OTP_TTL = 300; // 5 minutes
|
||||||
|
const RATE_LIMIT_TTL = 60; // 1 per minute per phone
|
||||||
|
|
||||||
|
function generateCode(): string {
|
||||||
|
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller('api/v1/auth/sms')
|
||||||
|
export class SmsController {
|
||||||
|
constructor(
|
||||||
|
private readonly smsService: SmsService,
|
||||||
|
@Inject(REDIS_CLIENT) private readonly redis: Redis,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/v1/auth/sms/send
|
||||||
|
* Send OTP to phone. Rate-limited: 1 per minute per phone number.
|
||||||
|
* purpose: 'register' | 'login'
|
||||||
|
*/
|
||||||
|
@Post('send')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async sendOtp(@Body() body: { phone: string; purpose?: string }) {
|
||||||
|
const phone = (body.phone || '').trim();
|
||||||
|
if (!phone) throw new BadRequestException('手机号不能为空');
|
||||||
|
|
||||||
|
const rateKey = `sms:rate:${phone}`;
|
||||||
|
const already = await this.redis.get(rateKey);
|
||||||
|
if (already) {
|
||||||
|
throw new BadRequestException('发送太频繁,请 60 秒后再试');
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = generateCode();
|
||||||
|
const otpKey = `sms:otp:${phone}`;
|
||||||
|
|
||||||
|
// Store OTP first, then send (avoid race condition)
|
||||||
|
await this.redis.set(otpKey, code, 'EX', OTP_TTL);
|
||||||
|
await this.redis.set(rateKey, '1', 'EX', RATE_LIMIT_TTL);
|
||||||
|
|
||||||
|
const result = await this.smsService.sendVerificationCode(phone, code);
|
||||||
|
if (!result.success) {
|
||||||
|
// Clean up if SMS actually failed
|
||||||
|
await this.redis.del(otpKey);
|
||||||
|
await this.redis.del(rateKey);
|
||||||
|
throw new BadRequestException(result.message || '短信发送失败,请稍后重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { message: '验证码已发送', expiresIn: OTP_TTL };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/v1/auth/sms/verify
|
||||||
|
* Verify OTP only (does not log in). Returns 200 on success.
|
||||||
|
*/
|
||||||
|
@Post('verify')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyOtp(@Body() body: { phone: string; code: string }) {
|
||||||
|
const phone = (body.phone || '').trim();
|
||||||
|
const code = (body.code || '').trim();
|
||||||
|
if (!phone || !code) throw new BadRequestException('手机号和验证码不能为空');
|
||||||
|
|
||||||
|
const otpKey = `sms:otp:${phone}`;
|
||||||
|
const stored = await this.redis.get(otpKey);
|
||||||
|
|
||||||
|
if (!stored || stored !== code) {
|
||||||
|
throw new BadRequestException('验证码错误或已过期');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { verified: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
299
pnpm-lock.yaml
299
pnpm-lock.yaml
|
|
@ -190,6 +190,15 @@ importers:
|
||||||
|
|
||||||
packages/services/auth-service:
|
packages/services/auth-service:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@alicloud/dysmsapi20170525':
|
||||||
|
specifier: ^4.3.1
|
||||||
|
version: 4.5.0
|
||||||
|
'@alicloud/openapi-client':
|
||||||
|
specifier: ^0.4.15
|
||||||
|
version: 0.4.15
|
||||||
|
'@alicloud/tea-util':
|
||||||
|
specifier: ^1.4.11
|
||||||
|
version: 1.4.11
|
||||||
'@it0/common':
|
'@it0/common':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../shared/common
|
version: link:../../shared/common
|
||||||
|
|
@ -223,6 +232,9 @@ importers:
|
||||||
bcryptjs:
|
bcryptjs:
|
||||||
specifier: ^2.4.3
|
specifier: ^2.4.3
|
||||||
version: 2.4.3
|
version: 2.4.3
|
||||||
|
ioredis:
|
||||||
|
specifier: ^5.3.0
|
||||||
|
version: 5.9.2
|
||||||
passport:
|
passport:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
|
|
@ -834,6 +846,60 @@ importers:
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@alicloud/credentials@2.4.4':
|
||||||
|
resolution: {integrity: sha512-/eRAGSKcniLIFQ1UCpDhB/IrHUZisQ1sc65ws/c2avxUMpXwH1rWAohb76SVAUJhiF4mwvLzLJM1Mn1XL4Xe/Q==}
|
||||||
|
|
||||||
|
'@alicloud/darabonba-array@0.1.2':
|
||||||
|
resolution: {integrity: sha512-ZPuQ+bJyjrd8XVVm55kl+ypk7OQoi1ZH/DiToaAEQaGvgEjrTcvQkg71//vUX/6cvbLIF5piQDvhrLb+lUEIPQ==}
|
||||||
|
|
||||||
|
'@alicloud/darabonba-encode-util@0.0.1':
|
||||||
|
resolution: {integrity: sha512-Sl5vCRVAYMqwmvXpJLM9hYoCHOMsQlGxaWSGhGWulpKk/NaUBArtoO1B0yHruJf1C5uHhEJIaylYcM48icFHgw==}
|
||||||
|
|
||||||
|
'@alicloud/darabonba-encode-util@0.0.2':
|
||||||
|
resolution: {integrity: sha512-mlsNctkeqmR0RtgE1Rngyeadi5snLOAHBCWEtYf68d7tyKskosXDTNeZ6VCD/UfrUu4N51ItO8zlpfXiOgeg3A==}
|
||||||
|
|
||||||
|
'@alicloud/darabonba-map@0.0.1':
|
||||||
|
resolution: {integrity: sha512-2ep+G3YDvuI+dRYVlmER1LVUQDhf9kEItmVB/bbEu1pgKzelcocCwAc79XZQjTcQGFgjDycf3vH87WLDGLFMlw==}
|
||||||
|
|
||||||
|
'@alicloud/darabonba-signature-util@0.0.4':
|
||||||
|
resolution: {integrity: sha512-I1TtwtAnzLamgqnAaOkN0IGjwkiti//0a7/auyVThdqiC/3kyafSAn6znysWOmzub4mrzac2WiqblZKFcN5NWg==}
|
||||||
|
|
||||||
|
'@alicloud/darabonba-string@1.0.3':
|
||||||
|
resolution: {integrity: sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA==}
|
||||||
|
|
||||||
|
'@alicloud/dysmsapi20170525@4.5.0':
|
||||||
|
resolution: {integrity: sha512-nhKdRDLRDhTVxr7VbMbBi6UtJWmVFgwySU2ohkJ1zL7jd98DEGGy8CE/n7W44ZP9+yTBBmLhM8qW1C12kHDEIg==}
|
||||||
|
|
||||||
|
'@alicloud/endpoint-util@0.0.1':
|
||||||
|
resolution: {integrity: sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==}
|
||||||
|
|
||||||
|
'@alicloud/gateway-pop@0.0.6':
|
||||||
|
resolution: {integrity: sha512-KF4I+JvfYuLKc3fWeWYIZ7lOVJ9jRW0sQXdXidZn1DKZ978ncfGf7i0LBfONGk4OxvNb/HD3/0yYhkgZgPbKtA==}
|
||||||
|
|
||||||
|
'@alicloud/gateway-spi@0.0.8':
|
||||||
|
resolution: {integrity: sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==}
|
||||||
|
|
||||||
|
'@alicloud/openapi-client@0.4.15':
|
||||||
|
resolution: {integrity: sha512-4VE0/k5ZdQbAhOSTqniVhuX1k5DUeUMZv74degn3wIWjLY6Bq+hxjaGsaHYlLZ2gA5wUrs8NcI5TE+lIQS3iiA==}
|
||||||
|
|
||||||
|
'@alicloud/openapi-core@1.0.7':
|
||||||
|
resolution: {integrity: sha512-I80PQVfmlzRiXGHwutMp2zTpiqUVv8ts30nWAfksfHUSTIapk3nj9IXaPbULMPGNV6xqEyshO2bj2a+pmwc2tQ==}
|
||||||
|
|
||||||
|
'@alicloud/openapi-util@0.3.3':
|
||||||
|
resolution: {integrity: sha512-vf0cQ/q8R2U7ZO88X5hDiu1yV3t/WexRj+YycWxRutkH/xVXfkmpRgps8lmNEk7Ar+0xnY8+daN2T+2OyB9F4A==}
|
||||||
|
|
||||||
|
'@alicloud/tea-typescript@1.8.0':
|
||||||
|
resolution: {integrity: sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==}
|
||||||
|
|
||||||
|
'@alicloud/tea-util@1.4.11':
|
||||||
|
resolution: {integrity: sha512-HyPEEQ8F0WoZegiCp7sVdrdm6eBOB+GCvGl4182u69LDFktxfirGLcAx3WExUr1zFWkq2OSmBroTwKQ4w/+Yww==}
|
||||||
|
|
||||||
|
'@alicloud/tea-util@1.4.9':
|
||||||
|
resolution: {integrity: sha512-S0wz76rGtoPKskQtRTGqeuqBHFj8BqUn0Vh+glXKun2/9UpaaaWmuJwcmtImk6bJZfLYEShDF/kxDmDJoNYiTw==}
|
||||||
|
|
||||||
|
'@alicloud/tea-xml@0.0.3':
|
||||||
|
resolution: {integrity: sha512-+/9GliugjrLglsXVrd1D80EqqKgGpyA0eQ6+1ZdUOYCaRguaSwz44trX3PaxPu/HhIPJg9PsGQQ3cSLXWZjbAA==}
|
||||||
|
|
||||||
'@angular-devkit/core@17.3.11':
|
'@angular-devkit/core@17.3.11':
|
||||||
resolution: {integrity: sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==}
|
resolution: {integrity: sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==}
|
||||||
engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
|
engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
|
||||||
|
|
@ -1033,6 +1099,9 @@ packages:
|
||||||
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
||||||
engines: {node: '>=0.1.90'}
|
engines: {node: '>=0.1.90'}
|
||||||
|
|
||||||
|
'@darabonba/typescript@1.0.4':
|
||||||
|
resolution: {integrity: sha512-icl8RGTw4DiWRpco6dVh21RS0IqrH4s/eEV36TZvz/e1+paogSZjaAgox7ByrlEuvG+bo5d8miq/dRlqiUaL/w==}
|
||||||
|
|
||||||
'@fidm/asn1@1.0.4':
|
'@fidm/asn1@1.0.4':
|
||||||
resolution: {integrity: sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==}
|
resolution: {integrity: sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
@ -1558,12 +1627,18 @@ packages:
|
||||||
'@types/node-fetch@2.6.13':
|
'@types/node-fetch@2.6.13':
|
||||||
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
|
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
|
||||||
|
|
||||||
|
'@types/node@12.20.55':
|
||||||
|
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
||||||
|
|
||||||
'@types/node@18.19.130':
|
'@types/node@18.19.130':
|
||||||
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
||||||
|
|
||||||
'@types/node@20.19.33':
|
'@types/node@20.19.33':
|
||||||
resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==}
|
resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==}
|
||||||
|
|
||||||
|
'@types/node@22.19.15':
|
||||||
|
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
||||||
|
|
||||||
'@types/nodemailer@6.4.22':
|
'@types/nodemailer@6.4.22':
|
||||||
resolution: {integrity: sha512-HV16KRsW7UyZBITE07B62k8PRAKFqRSFXn1T7vslurVjN761tMDBhk5Lbt17ehyTzK6XcyJnAgUpevrvkcVOzw==}
|
resolution: {integrity: sha512-HV16KRsW7UyZBITE07B62k8PRAKFqRSFXn1T7vslurVjN761tMDBhk5Lbt17ehyTzK6XcyJnAgUpevrvkcVOzw==}
|
||||||
|
|
||||||
|
|
@ -1607,6 +1682,9 @@ packages:
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||||
|
|
||||||
|
'@types/xml2js@0.4.14':
|
||||||
|
resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==}
|
||||||
|
|
||||||
'@types/yargs-parser@21.0.3':
|
'@types/yargs-parser@21.0.3':
|
||||||
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
|
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
|
||||||
|
|
||||||
|
|
@ -2611,6 +2689,9 @@ packages:
|
||||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
httpx@2.3.3:
|
||||||
|
resolution: {integrity: sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==}
|
||||||
|
|
||||||
human-signals@2.1.0:
|
human-signals@2.1.0:
|
||||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||||
engines: {node: '>=10.17.0'}
|
engines: {node: '>=10.17.0'}
|
||||||
|
|
@ -2645,6 +2726,9 @@ packages:
|
||||||
inherits@2.0.4:
|
inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
|
ini@1.3.8:
|
||||||
|
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||||
|
|
||||||
inquirer@8.2.6:
|
inquirer@8.2.6:
|
||||||
resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==}
|
resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
@ -2970,6 +3054,9 @@ packages:
|
||||||
jws@4.0.1:
|
jws@4.0.1:
|
||||||
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||||
|
|
||||||
|
kitx@2.2.0:
|
||||||
|
resolution: {integrity: sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg==}
|
||||||
|
|
||||||
kleur@3.0.3:
|
kleur@3.0.3:
|
||||||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -3140,6 +3227,12 @@ packages:
|
||||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
moment-timezone@0.5.48:
|
||||||
|
resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==}
|
||||||
|
|
||||||
|
moment@2.30.1:
|
||||||
|
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
|
||||||
|
|
@ -3584,6 +3677,10 @@ packages:
|
||||||
safer-buffer@2.1.2:
|
safer-buffer@2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
|
sax@1.5.0:
|
||||||
|
resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==}
|
||||||
|
engines: {node: '>=11.0.0'}
|
||||||
|
|
||||||
schema-utils@3.3.0:
|
schema-utils@3.3.0:
|
||||||
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
|
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
|
||||||
engines: {node: '>= 10.13.0'}
|
engines: {node: '>= 10.13.0'}
|
||||||
|
|
@ -3669,6 +3766,9 @@ packages:
|
||||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
sm3@1.0.3:
|
||||||
|
resolution: {integrity: sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==}
|
||||||
|
|
||||||
snake-case@3.0.4:
|
snake-case@3.0.4:
|
||||||
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
|
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
|
||||||
|
|
||||||
|
|
@ -4249,6 +4349,14 @@ packages:
|
||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
xml2js@0.6.2:
|
||||||
|
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
xmlbuilder@11.0.1:
|
||||||
|
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
xmlbuilder@13.0.2:
|
xmlbuilder@13.0.2:
|
||||||
resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==}
|
resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
|
|
@ -4292,6 +4400,146 @@ packages:
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@alicloud/credentials@2.4.4':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
httpx: 2.3.3
|
||||||
|
ini: 1.3.8
|
||||||
|
kitx: 2.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/darabonba-array@0.1.2':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/darabonba-encode-util@0.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
moment: 2.30.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/darabonba-encode-util@0.0.2':
|
||||||
|
dependencies:
|
||||||
|
moment: 2.30.1
|
||||||
|
|
||||||
|
'@alicloud/darabonba-map@0.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/darabonba-signature-util@0.0.4':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/darabonba-encode-util': 0.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/darabonba-string@1.0.3':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/dysmsapi20170525@4.5.0':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/openapi-core': 1.0.7
|
||||||
|
'@darabonba/typescript': 1.0.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/endpoint-util@0.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
kitx: 2.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/gateway-pop@0.0.6':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/credentials': 2.4.4
|
||||||
|
'@alicloud/darabonba-array': 0.1.2
|
||||||
|
'@alicloud/darabonba-encode-util': 0.0.2
|
||||||
|
'@alicloud/darabonba-map': 0.0.1
|
||||||
|
'@alicloud/darabonba-signature-util': 0.0.4
|
||||||
|
'@alicloud/darabonba-string': 1.0.3
|
||||||
|
'@alicloud/endpoint-util': 0.0.1
|
||||||
|
'@alicloud/gateway-spi': 0.0.8
|
||||||
|
'@alicloud/openapi-util': 0.3.3
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
'@alicloud/tea-util': 1.4.11
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/gateway-spi@0.0.8':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/credentials': 2.4.4
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/openapi-client@0.4.15':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/credentials': 2.4.4
|
||||||
|
'@alicloud/gateway-spi': 0.0.8
|
||||||
|
'@alicloud/openapi-util': 0.3.3
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
'@alicloud/tea-util': 1.4.9
|
||||||
|
'@alicloud/tea-xml': 0.0.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/openapi-core@1.0.7':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/credentials': 2.4.4
|
||||||
|
'@alicloud/gateway-pop': 0.0.6
|
||||||
|
'@alicloud/gateway-spi': 0.0.8
|
||||||
|
'@darabonba/typescript': 1.0.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/openapi-util@0.3.3':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
'@alicloud/tea-util': 1.4.11
|
||||||
|
kitx: 2.2.0
|
||||||
|
sm3: 1.0.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/tea-typescript@1.8.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 12.20.55
|
||||||
|
httpx: 2.3.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/tea-util@1.4.11':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
'@darabonba/typescript': 1.0.4
|
||||||
|
kitx: 2.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/tea-util@1.4.9':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
kitx: 2.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@alicloud/tea-xml@0.0.3':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
'@types/xml2js': 0.4.14
|
||||||
|
xml2js: 0.6.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@angular-devkit/core@17.3.11(chokidar@3.6.0)':
|
'@angular-devkit/core@17.3.11(chokidar@3.6.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 8.12.0
|
ajv: 8.12.0
|
||||||
|
|
@ -4544,6 +4792,17 @@ snapshots:
|
||||||
'@colors/colors@1.5.0':
|
'@colors/colors@1.5.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@darabonba/typescript@1.0.4':
|
||||||
|
dependencies:
|
||||||
|
'@alicloud/tea-typescript': 1.8.0
|
||||||
|
httpx: 2.3.3
|
||||||
|
lodash: 4.17.23
|
||||||
|
moment: 2.30.1
|
||||||
|
moment-timezone: 0.5.48
|
||||||
|
xml2js: 0.6.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@fidm/asn1@1.0.4': {}
|
'@fidm/asn1@1.0.4': {}
|
||||||
|
|
||||||
'@fidm/x509@1.2.1':
|
'@fidm/x509@1.2.1':
|
||||||
|
|
@ -5178,6 +5437,8 @@ snapshots:
|
||||||
'@types/node': 20.19.33
|
'@types/node': 20.19.33
|
||||||
form-data: 4.0.5
|
form-data: 4.0.5
|
||||||
|
|
||||||
|
'@types/node@12.20.55': {}
|
||||||
|
|
||||||
'@types/node@18.19.130':
|
'@types/node@18.19.130':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 5.26.5
|
undici-types: 5.26.5
|
||||||
|
|
@ -5186,6 +5447,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
'@types/node@22.19.15':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/nodemailer@6.4.22':
|
'@types/nodemailer@6.4.22':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.33
|
'@types/node': 20.19.33
|
||||||
|
|
@ -5245,6 +5510,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.33
|
'@types/node': 20.19.33
|
||||||
|
|
||||||
|
'@types/xml2js@0.4.14':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.19.33
|
||||||
|
|
||||||
'@types/yargs-parser@21.0.3': {}
|
'@types/yargs-parser@21.0.3': {}
|
||||||
|
|
||||||
'@types/yargs@17.0.35':
|
'@types/yargs@17.0.35':
|
||||||
|
|
@ -6365,6 +6634,13 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
httpx@2.3.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.19.33
|
||||||
|
debug: 4.4.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
human-signals@2.1.0: {}
|
human-signals@2.1.0: {}
|
||||||
|
|
||||||
humanize-ms@1.2.1:
|
humanize-ms@1.2.1:
|
||||||
|
|
@ -6396,6 +6672,8 @@ snapshots:
|
||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
ini@1.3.8: {}
|
||||||
|
|
||||||
inquirer@8.2.6:
|
inquirer@8.2.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-escapes: 4.3.2
|
ansi-escapes: 4.3.2
|
||||||
|
|
@ -6954,6 +7232,10 @@ snapshots:
|
||||||
jwa: 2.0.1
|
jwa: 2.0.1
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
kitx@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.19.15
|
||||||
|
|
||||||
kleur@3.0.3: {}
|
kleur@3.0.3: {}
|
||||||
|
|
||||||
leven@3.1.0: {}
|
leven@3.1.0: {}
|
||||||
|
|
@ -7078,6 +7360,12 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
|
|
||||||
|
moment-timezone@0.5.48:
|
||||||
|
dependencies:
|
||||||
|
moment: 2.30.1
|
||||||
|
|
||||||
|
moment@2.30.1: {}
|
||||||
|
|
||||||
ms@2.0.0: {}
|
ms@2.0.0: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
@ -7497,6 +7785,8 @@ snapshots:
|
||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
|
sax@1.5.0: {}
|
||||||
|
|
||||||
schema-utils@3.3.0:
|
schema-utils@3.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
|
|
@ -7608,6 +7898,8 @@ snapshots:
|
||||||
|
|
||||||
slash@3.0.0: {}
|
slash@3.0.0: {}
|
||||||
|
|
||||||
|
sm3@1.0.3: {}
|
||||||
|
|
||||||
snake-case@3.0.4:
|
snake-case@3.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
dot-case: 3.0.4
|
dot-case: 3.0.4
|
||||||
|
|
@ -8140,6 +8432,13 @@ snapshots:
|
||||||
|
|
||||||
ws@8.18.3: {}
|
ws@8.18.3: {}
|
||||||
|
|
||||||
|
xml2js@0.6.2:
|
||||||
|
dependencies:
|
||||||
|
sax: 1.5.0
|
||||||
|
xmlbuilder: 11.0.1
|
||||||
|
|
||||||
|
xmlbuilder@11.0.1: {}
|
||||||
|
|
||||||
xmlbuilder@13.0.2: {}
|
xmlbuilder@13.0.2: {}
|
||||||
|
|
||||||
xmlbuilder@15.1.1: {}
|
xmlbuilder@15.1.1: {}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue