fix:页面和node后端ok;

This commit is contained in:
grabbit 2025-08-04 01:02:29 +08:00
parent ca7e92a1a1
commit 3a014a3d20
14 changed files with 1000 additions and 717 deletions

View File

@ -32,14 +32,47 @@ export function AuthProvider({ children }: AuthProviderProps) {
const isAuthenticated = !!user
const refreshTokens = async (): Promise<boolean> => {
const refreshToken = localStorage.getItem("refreshToken")
if (!refreshToken) {
return false
}
try {
const response = await fetch("http://localhost:3001/api/v1/auth/refresh", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ refreshToken }),
})
if (!response.ok) {
localStorage.removeItem("accessToken")
localStorage.removeItem("refreshToken")
return false
}
const data = await response.json()
localStorage.setItem("accessToken", data.accessToken)
localStorage.setItem("refreshToken", data.refreshToken)
return true
} catch (error) {
console.error("Failed to refresh tokens:", error)
localStorage.removeItem("accessToken")
localStorage.removeItem("refreshToken")
return false
}
}
const fetchUserProfile = async (): Promise<User | null> => {
const token = localStorage.getItem("accessToken")
let token = localStorage.getItem("accessToken")
if (!token) {
return null
}
try {
const response = await fetch("http://localhost:3000/api/v1/auth/profile", {
let response = await fetch("http://localhost:3001/api/v1/auth/profile", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
@ -47,8 +80,29 @@ export function AuthProvider({ children }: AuthProviderProps) {
},
})
// If token is expired, try to refresh
if (response.status === 401) {
const refreshed = await refreshTokens()
if (!refreshed) {
return null
}
// Retry with new token
token = localStorage.getItem("accessToken")
if (!token) {
return null
}
response = await fetch("http://localhost:3001/api/v1/auth/profile", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
})
}
if (!response.ok) {
// Token might be invalid, remove it
localStorage.removeItem("accessToken")
localStorage.removeItem("refreshToken")
return null
@ -63,7 +117,6 @@ export function AuthProvider({ children }: AuthProviderProps) {
hasActiveSubscription: profileData.hasActiveSubscription,
}
} catch (error) {
// Network error or other issue
console.error("Failed to fetch user profile:", error)
return null
}
@ -72,7 +125,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
const login = async (email: string, password: string) => {
setIsLoading(true)
try {
const response = await fetch("http://localhost:3000/api/v1/auth/login-email", {
const response = await fetch("http://localhost:3001/api/v1/auth/login-email", {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -113,7 +166,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
const register = async (email: string, password: string, displayName: string) => {
setIsLoading(true)
try {
const response = await fetch("http://localhost:3000/api/v1/auth/register-email", {
const response = await fetch("http://localhost:3001/api/v1/auth/register-email", {
method: "POST",
headers: {
"Content-Type": "application/json",

View File

@ -7,7 +7,7 @@ export const devicesApi = {
throw new Error("No access token found")
}
const response = await fetch("http://localhost:3000/api/v1/devices", {
const response = await fetch("http://localhost:3001/api/v1/devices", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,

View File

@ -31,7 +31,7 @@ export const eventsApi = {
throw new Error("No access token found")
}
const url = new URL("http://localhost:3000/api/v1/events")
const url = new URL("http://localhost:3001/api/v1/events")
if (params.limit) {
url.searchParams.append("limit", params.limit.toString())
@ -69,7 +69,7 @@ export const eventsApi = {
throw new Error("No access token found")
}
const response = await fetch(`http://localhost:3000/api/v1/events/${eventId}`, {
const response = await fetch(`http://localhost:3001/api/v1/events/${eventId}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,

View File

@ -37,7 +37,7 @@ export const subscriptionApi = {
throw new Error("No access token found")
}
const response = await fetch("http://localhost:3000/api/v1/payments/subscription", {
const response = await fetch("http://localhost:3001/api/v1/payments/subscription", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
@ -61,7 +61,7 @@ export const subscriptionApi = {
throw new Error("No access token found")
}
const response = await fetch("http://localhost:3000/api/v1/payments/customer-portal", {
const response = await fetch("http://localhost:3001/api/v1/payments/customer-portal", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
@ -86,7 +86,7 @@ export const subscriptionApi = {
throw new Error("No access token found")
}
const response = await fetch("http://localhost:3000/api/v1/payments/checkout-session/stripe", {
const response = await fetch("http://localhost:3001/api/v1/payments/checkout-session/stripe", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,

View File

@ -8,6 +8,7 @@ import {
HttpCode,
HttpStatus,
UseGuards,
UnauthorizedException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterEmailDto } from './dto/register-email.dto';
@ -24,7 +25,7 @@ export class AuthController {
@Post('register-email')
@HttpCode(HttpStatus.CREATED)
async registerWithEmail(@Body(ValidationPipe) registerDto: RegisterEmailDto) {
async registerWithEmail(@Body() registerDto: RegisterEmailDto) {
try {
const result = await this.authService.registerWithEmail(registerDto);
this.metricsService.recordAuthOperation('register', true, 'email');
@ -37,7 +38,7 @@ export class AuthController {
@Post('login-email')
@HttpCode(HttpStatus.OK)
async loginWithEmail(@Body(ValidationPipe) loginDto: LoginEmailDto) {
async loginWithEmail(@Body() loginDto: LoginEmailDto) {
try {
const result = await this.authService.loginWithEmail(loginDto);
this.metricsService.recordAuthOperation('login', true, 'email');
@ -54,4 +55,13 @@ export class AuthController {
const userId = req.user.userId;
return await this.authService.getUserProfile(userId);
}
@Post('refresh')
@HttpCode(HttpStatus.OK)
async refreshToken(@Body('refreshToken') refreshToken: string) {
if (!refreshToken) {
throw new UnauthorizedException('Refresh token is required');
}
return await this.authService.refreshToken(refreshToken);
}
}

View File

@ -8,17 +8,21 @@ import { JwtStrategy } from './strategies/jwt.strategy';
import { UserProfile } from '../entities/user-profile.entity';
import { UserIdentity } from '../entities/user-identity.entity';
import { PaymentsModule } from '../payments/payments.module';
import { MetricsModule } from '../metrics/metrics.module';
@Module({
imports: [
TypeOrmModule.forFeature([UserProfile, UserIdentity]),
PassportModule,
JwtModule.register({
secret:
process.env.JWT_ACCESS_SECRET || 'default-secret-change-in-production',
signOptions: { expiresIn: process.env.JWT_ACCESS_EXPIRATION || '15m' },
JwtModule.registerAsync({
useFactory: () => ({
secret:
process.env.JWT_ACCESS_SECRET || 'default-secret-change-in-production',
signOptions: { expiresIn: process.env.JWT_ACCESS_EXPIRATION || '2h' },
}),
}),
forwardRef(() => PaymentsModule),
MetricsModule,
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],

View File

@ -184,4 +184,55 @@ export class AuthService {
};
}
}
async refreshToken(refreshToken: string): Promise<{
accessToken: string;
refreshToken: string;
}> {
try {
// Verify the refresh token
const payload = this.jwtService.verify(refreshToken, {
secret: process.env.JWT_REFRESH_SECRET || 'default-refresh-secret',
});
// Check if user still exists
const userProfile = await this.userProfileRepository.findOne({
where: { id: payload.userId },
relations: ['identities'],
});
if (!userProfile) {
throw new UnauthorizedException('User not found');
}
// Get the email from user identity
const emailIdentity = userProfile.identities.find(
(identity) => identity.provider === 'email',
);
if (!emailIdentity) {
throw new UnauthorizedException('User email not found');
}
// Generate new tokens
const newPayload = {
userId: userProfile.id,
email: emailIdentity.email,
sub: userProfile.id,
};
const newAccessToken = this.jwtService.sign(newPayload);
const newRefreshToken = this.jwtService.sign(newPayload, {
secret: process.env.JWT_REFRESH_SECRET || 'default-refresh-secret',
expiresIn: process.env.JWT_REFRESH_EXPIRATION || '7d',
});
return {
accessToken: newAccessToken,
refreshToken: newRefreshToken,
};
} catch (error) {
throw new UnauthorizedException('Invalid refresh token');
}
}
}

View File

@ -22,5 +22,5 @@ export class RegisterEmailDto {
@IsString()
@IsNotEmpty({ message: 'Display name is required' })
displayName?: string;
displayName: string;
}

View File

@ -1,12 +1,17 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DevicesController } from './devices.controller';
import { DevicesService } from './devices.service';
import { Device } from '../entities/device.entity';
import { InventoryDevice } from '../entities/inventory-device.entity';
import { UserProfile } from '../entities/user-profile.entity';
import { PaymentsModule } from '../payments/payments.module';
@Module({
imports: [TypeOrmModule.forFeature([Device, InventoryDevice])],
imports: [
TypeOrmModule.forFeature([Device, InventoryDevice, UserProfile]),
forwardRef(() => PaymentsModule),
],
controllers: [DevicesController],
providers: [DevicesService],
exports: [DevicesService],

View File

@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { EventsController } from './events.controller';
import { EventsService } from './events.service';
@ -6,9 +6,14 @@ import { AwsService } from '../aws/aws.service';
import { RawEvent } from '../entities/raw-event.entity';
import { Device } from '../entities/device.entity';
import { ValidatedEvent } from '../entities/validated-event.entity';
import { UserProfile } from '../entities/user-profile.entity';
import { PaymentsModule } from '../payments/payments.module';
@Module({
imports: [TypeOrmModule.forFeature([RawEvent, Device, ValidatedEvent])],
imports: [
TypeOrmModule.forFeature([RawEvent, Device, ValidatedEvent, UserProfile]),
forwardRef(() => PaymentsModule),
],
controllers: [EventsController],
providers: [EventsService, AwsService],
exports: [EventsService, AwsService],

View File

@ -24,27 +24,30 @@ async function bootstrap() {
cwd: process.cwd(),
});
// Configure raw body parsing for webhook endpoints
app.use(
'/api/v1/payments/webhook',
json({
verify: (req: any, res, buf) => {
req.rawBody = buf;
},
}),
);
// fixme 打开后json反序列化失败
// // Configure raw body parsing for webhook endpoints
// app.use(
// '/api/v1/payments/webhook',
// json({
// verify: (req: any, res, buf) => {
// req.rawBody = buf;
// },
// }),
// );
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
skipMissingProperties: false,
validationError: { target: false },
}),
);
// Enable CORS for frontend
app.enableCors({
origin: 'http://localhost:3001', // Adjust if your frontend runs on different port
origin: 'http://localhost:3000', // Frontend runs on port 3000
credentials: true,
});

View File

@ -182,6 +182,25 @@ export class PaymentsService {
};
}
// Development/Test mode: if subscription ID starts with 'sub_test_', return mock data
if (userProfile.paymentProviderSubscriptionId.startsWith('sub_test_')) {
this.logger.log(`Returning mock subscription data for development mode`);
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
return {
hasActiveSubscription: true,
subscriptionStatus: 'active',
currentPlan: {
id: userProfile.paymentProviderSubscriptionId,
priceId: 'price_test_premium',
name: 'Premium Plan (Test)',
},
nextBillingDate: nextMonth,
customerId: userProfile.paymentProviderCustomerId,
};
}
try {
const provider = this.getProvider('stripe');
const subscription = await provider.getSubscription(

1494
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -29,5 +29,8 @@
"workspaces": [
"meteor-web-backend",
"meteor-frontend"
]
}
],
"dependencies": {
"node-fetch": "^2.7.0"
}
}