feat: 添加忘记密码功能
后端: - 新增 MailService 集成 Nodemailer 发送邮件 - 新增 EmailCodeService 管理邮箱验证码 - 新增 sendResetPasswordEmail 和 resetPassword 接口 - 支持验证码过期和次数限制 前端: - 新增忘记密码页面 (forgot-password) - 新增 ForgotPasswordForm 组件,支持邮箱验证和密码重置 - 更新 auth.service.ts 添加相关 API 调用 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,12 @@ import { AuthService } from './auth.service';
|
|||||||
import { CurrentUser } from './decorators/current-user.decorator';
|
import { CurrentUser } from './decorators/current-user.decorator';
|
||||||
import { Public } from './decorators/public.decorator';
|
import { Public } from './decorators/public.decorator';
|
||||||
import { RegisterDto, LoginDto, RefreshTokenDto, AuthResponseDto, AuthUserDto, RefreshTokenResponseDto } from './dto/auth.dto';
|
import { RegisterDto, LoginDto, RefreshTokenDto, AuthResponseDto, AuthUserDto, RefreshTokenResponseDto } from './dto/auth.dto';
|
||||||
|
import {
|
||||||
|
SendResetPasswordEmailDto,
|
||||||
|
SendResetPasswordEmailResponseDto,
|
||||||
|
ResetPasswordDto,
|
||||||
|
ResetPasswordResponseDto,
|
||||||
|
} from './dto/reset-password.dto';
|
||||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||||
|
|
||||||
import { UserMenusAndPermissionsResponseDto } from '@/permission/dto/menu.dto';
|
import { UserMenusAndPermissionsResponseDto } from '@/permission/dto/menu.dto';
|
||||||
@@ -70,4 +76,22 @@ export class AuthController {
|
|||||||
user.roleIds
|
user.roleIds
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('send-reset-email')
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: '发送重置密码邮件' })
|
||||||
|
@ApiBody({ type: SendResetPasswordEmailDto })
|
||||||
|
@ApiOkResponse({ type: SendResetPasswordEmailResponseDto, description: '发送成功' })
|
||||||
|
sendResetPasswordEmail(@Body() dto: SendResetPasswordEmailDto) {
|
||||||
|
return this.authService.sendResetPasswordEmail(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('reset-password')
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: '重置密码' })
|
||||||
|
@ApiBody({ type: ResetPasswordDto })
|
||||||
|
@ApiOkResponse({ type: ResetPasswordResponseDto, description: '重置成功' })
|
||||||
|
resetPassword(@Body() dto: ResetPasswordDto) {
|
||||||
|
return this.authService.resetPassword(dto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
apps/api/src/auth/dto/reset-password.dto.ts
Normal file
54
apps/api/src/auth/dto/reset-password.dto.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import type {
|
||||||
|
SendResetPasswordEmailDto as ISendResetPasswordEmailDto,
|
||||||
|
SendResetPasswordEmailResponse as ISendResetPasswordEmailResponse,
|
||||||
|
ResetPasswordDto as IResetPasswordDto,
|
||||||
|
ResetPasswordResponse as IResetPasswordResponse,
|
||||||
|
} from '@seclusion/shared';
|
||||||
|
import { IsEmail, IsString, IsNotEmpty, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class SendResetPasswordEmailDto implements ISendResetPasswordEmailDto {
|
||||||
|
@ApiProperty({ example: 'user@example.com', description: '用户邮箱' })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'cap_abc123xyz456def', description: '图形验证码 ID' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
captchaId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'ab12', description: '图形验证码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
captchaCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendResetPasswordEmailResponseDto implements ISendResetPasswordEmailResponse {
|
||||||
|
@ApiProperty({ example: 'emc_abc123xyz456def', description: '邮箱验证码 ID' })
|
||||||
|
emailCodeId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 600, description: '验证码有效期(秒)' })
|
||||||
|
expiresIn: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResetPasswordDto implements IResetPasswordDto {
|
||||||
|
@ApiProperty({ example: 'emc_abc123xyz456def', description: '邮箱验证码 ID' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
emailCodeId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '123456', description: '邮箱验证码(6位数字)' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
emailCode: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'newPassword123', description: '新密码(至少6位)' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(6)
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResetPasswordResponseDto implements IResetPasswordResponse {
|
||||||
|
@ApiProperty({ example: '密码重置成功', description: '响应消息' })
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
10
apps/api/src/common/email-code/email-code.module.ts
Normal file
10
apps/api/src/common/email-code/email-code.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { EmailCodeService } from './email-code.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [EmailCodeService],
|
||||||
|
exports: [EmailCodeService],
|
||||||
|
})
|
||||||
|
export class EmailCodeModule {}
|
||||||
70
apps/api/src/common/email-code/email-code.service.ts
Normal file
70
apps/api/src/common/email-code/email-code.service.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
|
||||||
|
import { customAlphabet } from 'nanoid';
|
||||||
|
|
||||||
|
import { RedisService } from '../redis/redis.service';
|
||||||
|
|
||||||
|
// 生成 6 位数字验证码
|
||||||
|
const generateCode = customAlphabet('0123456789', 6);
|
||||||
|
// 生成验证码 ID
|
||||||
|
const generateCodeId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 16);
|
||||||
|
|
||||||
|
// 验证码有效期(秒)
|
||||||
|
const EMAIL_CODE_TTL = 600; // 10 分钟
|
||||||
|
// Redis key 前缀
|
||||||
|
const EMAIL_CODE_PREFIX = 'email_code:';
|
||||||
|
|
||||||
|
interface EmailCodeData {
|
||||||
|
code: string;
|
||||||
|
email: string;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EmailCodeService {
|
||||||
|
private readonly logger = new Logger(EmailCodeService.name);
|
||||||
|
|
||||||
|
constructor(private readonly redisService: RedisService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成邮箱验证码
|
||||||
|
* @returns 验证码 ID 和验证码
|
||||||
|
*/
|
||||||
|
async generate(email: string): Promise<{ emailCodeId: string; code: string; expiresIn: number }> {
|
||||||
|
const code = generateCode();
|
||||||
|
const emailCodeId = `emc_${generateCodeId()}`;
|
||||||
|
|
||||||
|
const data: EmailCodeData = {
|
||||||
|
code,
|
||||||
|
email,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.redisService.setJson(`${EMAIL_CODE_PREFIX}${emailCodeId}`, data, EMAIL_CODE_TTL);
|
||||||
|
this.logger.debug(`邮箱验证码已生成: ${emailCodeId}, email: ${email}`);
|
||||||
|
|
||||||
|
return { emailCodeId, code, expiresIn: EMAIL_CODE_TTL };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证邮箱验证码
|
||||||
|
* @returns 验证成功返回邮箱地址
|
||||||
|
*/
|
||||||
|
async verify(emailCodeId: string, code: string): Promise<string> {
|
||||||
|
const key = `${EMAIL_CODE_PREFIX}${emailCodeId}`;
|
||||||
|
const data = await this.redisService.getJson<EmailCodeData>(key);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
throw new BadRequestException('验证码不存在或已过期');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.code !== code) {
|
||||||
|
throw new BadRequestException('验证码错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证成功后删除(一次性使用)
|
||||||
|
await this.redisService.del(key);
|
||||||
|
this.logger.debug(`邮箱验证码验证成功: ${emailCodeId}`);
|
||||||
|
|
||||||
|
return data.email;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/api/src/common/mail/mail.module.ts
Normal file
10
apps/api/src/common/mail/mail.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { MailService } from './mail.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [MailService],
|
||||||
|
exports: [MailService],
|
||||||
|
})
|
||||||
|
export class MailModule {}
|
||||||
50
apps/api/src/common/mail/mail.service.ts
Normal file
50
apps/api/src/common/mail/mail.service.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import * as nodemailer from 'nodemailer';
|
||||||
|
import type { Transporter } from 'nodemailer';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MailService {
|
||||||
|
private readonly logger = new Logger(MailService.name);
|
||||||
|
private transporter: Transporter;
|
||||||
|
|
||||||
|
constructor(private configService: ConfigService) {
|
||||||
|
this.transporter = nodemailer.createTransport({
|
||||||
|
host: this.configService.get<string>('SMTP_HOST'),
|
||||||
|
port: this.configService.get<number>('SMTP_PORT', 587),
|
||||||
|
secure: this.configService.get<string>('SMTP_SECURE') === 'true',
|
||||||
|
auth: {
|
||||||
|
user: this.configService.get<string>('SMTP_USER'),
|
||||||
|
pass: this.configService.get<string>('SMTP_PASS'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送重置密码验证码邮件
|
||||||
|
*/
|
||||||
|
async sendResetPasswordCode(email: string, code: string): Promise<void> {
|
||||||
|
const from = this.configService.get<string>('SMTP_FROM', 'noreply@example.com');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.transporter.sendMail({
|
||||||
|
from,
|
||||||
|
to: email,
|
||||||
|
subject: '重置密码验证码',
|
||||||
|
html: `
|
||||||
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
|
||||||
|
<h2>重置密码</h2>
|
||||||
|
<p>您正在重置密码,验证码为:</p>
|
||||||
|
<p style="font-size: 24px; font-weight: bold; color: #333; letter-spacing: 4px;">${code}</p>
|
||||||
|
<p>验证码有效期为 10 分钟,请尽快使用。</p>
|
||||||
|
<p style="color: #666; font-size: 12px;">如果您没有请求重置密码,请忽略此邮件。</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
this.logger.log(`重置密码邮件已发送至: ${email}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`发送邮件失败: ${email}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
apps/web/src/app/(auth)/forgot-password/page.tsx
Normal file
43
apps/web/src/app/(auth)/forgot-password/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { ForgotPasswordForm } from '@/components/forms/ForgotPasswordForm';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: '忘记密码',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ForgotPasswordPage() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="space-y-1">
|
||||||
|
<CardTitle className="text-2xl text-center">忘记密码</CardTitle>
|
||||||
|
<CardDescription className="text-center">
|
||||||
|
输入您的邮箱,我们将发送验证码帮助您重置密码
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ForgotPasswordForm />
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex justify-center">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
想起密码了?{' '}
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="hover:text-primary underline underline-offset-4"
|
||||||
|
>
|
||||||
|
返回登录
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
222
apps/web/src/components/forms/ForgotPasswordForm.tsx
Normal file
222
apps/web/src/components/forms/ForgotPasswordForm.tsx
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { CaptchaScene } from '@seclusion/shared';
|
||||||
|
import { Loader2, ArrowLeft } from 'lucide-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import { Captcha, type CaptchaRef } from '@/components/shared/Captcha';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
forgotPasswordSchema,
|
||||||
|
resetPasswordSchema,
|
||||||
|
type ForgotPasswordFormValues,
|
||||||
|
type ResetPasswordFormValues,
|
||||||
|
} from '@/lib/validations';
|
||||||
|
import { authService } from '@/services/auth.service';
|
||||||
|
|
||||||
|
export function ForgotPasswordForm() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [step, setStep] = useState<1 | 2>(1);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [emailCodeId, setEmailCodeId] = useState('');
|
||||||
|
const captchaRef = useRef<CaptchaRef>(null);
|
||||||
|
|
||||||
|
// Step 1: 发送验证码表单
|
||||||
|
const step1Form = useForm<ForgotPasswordFormValues>({
|
||||||
|
resolver: zodResolver(forgotPasswordSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
captchaId: '',
|
||||||
|
captchaCode: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: 重置密码表单
|
||||||
|
const step2Form = useForm<ResetPasswordFormValues>({
|
||||||
|
resolver: zodResolver(resetPasswordSchema),
|
||||||
|
defaultValues: {
|
||||||
|
emailCode: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 1: 发送验证码
|
||||||
|
const onSendCode = async (values: ForgotPasswordFormValues) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await authService.sendResetPasswordEmail({
|
||||||
|
email: values.email,
|
||||||
|
captchaId: values.captchaId,
|
||||||
|
captchaCode: values.captchaCode,
|
||||||
|
});
|
||||||
|
setEmailCodeId(response.emailCodeId);
|
||||||
|
setStep(2);
|
||||||
|
toast.success('验证码已发送到您的邮箱');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : '发送失败');
|
||||||
|
captchaRef.current?.refresh();
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 2: 重置密码
|
||||||
|
const onResetPassword = async (values: ResetPasswordFormValues) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await authService.resetPassword({
|
||||||
|
emailCodeId,
|
||||||
|
emailCode: values.emailCode,
|
||||||
|
password: values.password,
|
||||||
|
});
|
||||||
|
toast.success('密码重置成功,请重新登录');
|
||||||
|
router.push('/login');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : '重置失败');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 1: 发送验证码表单
|
||||||
|
if (step === 1) {
|
||||||
|
return (
|
||||||
|
<Form {...step1Form} key="step1">
|
||||||
|
<form onSubmit={step1Form.handleSubmit(onSendCode)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={step1Form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>邮箱</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="请输入注册邮箱"
|
||||||
|
autoComplete="email"
|
||||||
|
disabled={isLoading}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Captcha
|
||||||
|
ref={captchaRef}
|
||||||
|
scene={CaptchaScene.RESET_PASSWORD}
|
||||||
|
value={step1Form.watch('captchaCode')}
|
||||||
|
onChange={(id, code) => {
|
||||||
|
step1Form.setValue('captchaId', id);
|
||||||
|
step1Form.setValue('captchaCode', code);
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||||
|
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
发送验证码
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: 重置密码表单
|
||||||
|
return (
|
||||||
|
<Form {...step2Form} key="step2">
|
||||||
|
<form onSubmit={step2Form.handleSubmit(onResetPassword)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={step2Form.control}
|
||||||
|
name="emailCode"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>邮箱验证码</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="请输入 6 位验证码"
|
||||||
|
maxLength={6}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={step2Form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>新密码</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入新密码"
|
||||||
|
autoComplete="new-password"
|
||||||
|
disabled={isLoading}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={step2Form.control}
|
||||||
|
name="confirmPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>确认密码</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入新密码"
|
||||||
|
autoComplete="new-password"
|
||||||
|
disabled={isLoading}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={() => setStep(1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
返回
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" className="flex-1" disabled={isLoading}>
|
||||||
|
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
重置密码
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,10 @@ import type {
|
|||||||
LoginDto,
|
LoginDto,
|
||||||
RefreshTokenResponse,
|
RefreshTokenResponse,
|
||||||
RegisterDto,
|
RegisterDto,
|
||||||
|
SendResetPasswordEmailDto,
|
||||||
|
SendResetPasswordEmailResponse,
|
||||||
|
ResetPasswordDto,
|
||||||
|
ResetPasswordResponse,
|
||||||
} from '@seclusion/shared';
|
} from '@seclusion/shared';
|
||||||
|
|
||||||
import { API_ENDPOINTS } from '@/config/constants';
|
import { API_ENDPOINTS } from '@/config/constants';
|
||||||
@@ -37,4 +41,22 @@ export const authService = {
|
|||||||
getMe: (): Promise<AuthUser> => {
|
getMe: (): Promise<AuthUser> => {
|
||||||
return http.get<AuthUser>(API_ENDPOINTS.AUTH.ME);
|
return http.get<AuthUser>(API_ENDPOINTS.AUTH.ME);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 发送重置密码邮件(公开接口,跳过认证)
|
||||||
|
sendResetPasswordEmail: (data: SendResetPasswordEmailDto): Promise<SendResetPasswordEmailResponse> => {
|
||||||
|
return http.post<SendResetPasswordEmailResponse>(
|
||||||
|
API_ENDPOINTS.AUTH.SEND_RESET_EMAIL,
|
||||||
|
data,
|
||||||
|
{ skipAuth: true }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重置密码(公开接口,跳过认证)
|
||||||
|
resetPassword: (data: ResetPasswordDto): Promise<ResetPasswordResponse> => {
|
||||||
|
return http.post<ResetPasswordResponse>(
|
||||||
|
API_ENDPOINTS.AUTH.RESET_PASSWORD,
|
||||||
|
data,
|
||||||
|
{ skipAuth: true }
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user