fix:页面和node后端ok;
This commit is contained in:
parent
ca7e92a1a1
commit
3a014a3d20
@ -32,14 +32,47 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
|
|
||||||
const isAuthenticated = !!user
|
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 fetchUserProfile = async (): Promise<User | null> => {
|
||||||
const token = localStorage.getItem("accessToken")
|
let token = localStorage.getItem("accessToken")
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${token}`,
|
"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) {
|
if (!response.ok) {
|
||||||
// Token might be invalid, remove it
|
|
||||||
localStorage.removeItem("accessToken")
|
localStorage.removeItem("accessToken")
|
||||||
localStorage.removeItem("refreshToken")
|
localStorage.removeItem("refreshToken")
|
||||||
return null
|
return null
|
||||||
@ -63,7 +117,6 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
hasActiveSubscription: profileData.hasActiveSubscription,
|
hasActiveSubscription: profileData.hasActiveSubscription,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Network error or other issue
|
|
||||||
console.error("Failed to fetch user profile:", error)
|
console.error("Failed to fetch user profile:", error)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -72,7 +125,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
const login = async (email: string, password: string) => {
|
const login = async (email: string, password: string) => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
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",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -113,7 +166,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
const register = async (email: string, password: string, displayName: string) => {
|
const register = async (email: string, password: string, displayName: string) => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
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",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export const devicesApi = {
|
|||||||
throw new Error("No access token found")
|
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",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${token}`,
|
"Authorization": `Bearer ${token}`,
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export const eventsApi = {
|
|||||||
throw new Error("No access token found")
|
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) {
|
if (params.limit) {
|
||||||
url.searchParams.append("limit", params.limit.toString())
|
url.searchParams.append("limit", params.limit.toString())
|
||||||
@ -69,7 +69,7 @@ export const eventsApi = {
|
|||||||
throw new Error("No access token found")
|
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",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${token}`,
|
"Authorization": `Bearer ${token}`,
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export const subscriptionApi = {
|
|||||||
throw new Error("No access token found")
|
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",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${token}`,
|
"Authorization": `Bearer ${token}`,
|
||||||
@ -61,7 +61,7 @@ export const subscriptionApi = {
|
|||||||
throw new Error("No access token found")
|
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",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${token}`,
|
"Authorization": `Bearer ${token}`,
|
||||||
@ -86,7 +86,7 @@ export const subscriptionApi = {
|
|||||||
throw new Error("No access token found")
|
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",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${token}`,
|
"Authorization": `Bearer ${token}`,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { RegisterEmailDto } from './dto/register-email.dto';
|
import { RegisterEmailDto } from './dto/register-email.dto';
|
||||||
@ -24,7 +25,7 @@ export class AuthController {
|
|||||||
|
|
||||||
@Post('register-email')
|
@Post('register-email')
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
async registerWithEmail(@Body(ValidationPipe) registerDto: RegisterEmailDto) {
|
async registerWithEmail(@Body() registerDto: RegisterEmailDto) {
|
||||||
try {
|
try {
|
||||||
const result = await this.authService.registerWithEmail(registerDto);
|
const result = await this.authService.registerWithEmail(registerDto);
|
||||||
this.metricsService.recordAuthOperation('register', true, 'email');
|
this.metricsService.recordAuthOperation('register', true, 'email');
|
||||||
@ -37,7 +38,7 @@ export class AuthController {
|
|||||||
|
|
||||||
@Post('login-email')
|
@Post('login-email')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async loginWithEmail(@Body(ValidationPipe) loginDto: LoginEmailDto) {
|
async loginWithEmail(@Body() loginDto: LoginEmailDto) {
|
||||||
try {
|
try {
|
||||||
const result = await this.authService.loginWithEmail(loginDto);
|
const result = await this.authService.loginWithEmail(loginDto);
|
||||||
this.metricsService.recordAuthOperation('login', true, 'email');
|
this.metricsService.recordAuthOperation('login', true, 'email');
|
||||||
@ -54,4 +55,13 @@ export class AuthController {
|
|||||||
const userId = req.user.userId;
|
const userId = req.user.userId;
|
||||||
return await this.authService.getUserProfile(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,17 +8,21 @@ import { JwtStrategy } from './strategies/jwt.strategy';
|
|||||||
import { UserProfile } from '../entities/user-profile.entity';
|
import { UserProfile } from '../entities/user-profile.entity';
|
||||||
import { UserIdentity } from '../entities/user-identity.entity';
|
import { UserIdentity } from '../entities/user-identity.entity';
|
||||||
import { PaymentsModule } from '../payments/payments.module';
|
import { PaymentsModule } from '../payments/payments.module';
|
||||||
|
import { MetricsModule } from '../metrics/metrics.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([UserProfile, UserIdentity]),
|
TypeOrmModule.forFeature([UserProfile, UserIdentity]),
|
||||||
PassportModule,
|
PassportModule,
|
||||||
JwtModule.register({
|
JwtModule.registerAsync({
|
||||||
secret:
|
useFactory: () => ({
|
||||||
process.env.JWT_ACCESS_SECRET || 'default-secret-change-in-production',
|
secret:
|
||||||
signOptions: { expiresIn: process.env.JWT_ACCESS_EXPIRATION || '15m' },
|
process.env.JWT_ACCESS_SECRET || 'default-secret-change-in-production',
|
||||||
|
signOptions: { expiresIn: process.env.JWT_ACCESS_EXPIRATION || '2h' },
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
forwardRef(() => PaymentsModule),
|
forwardRef(() => PaymentsModule),
|
||||||
|
MetricsModule,
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
providers: [AuthService, JwtStrategy],
|
providers: [AuthService, JwtStrategy],
|
||||||
|
|||||||
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,5 +22,5 @@ export class RegisterEmailDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty({ message: 'Display name is required' })
|
@IsNotEmpty({ message: 'Display name is required' })
|
||||||
displayName?: string;
|
displayName: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { DevicesController } from './devices.controller';
|
import { DevicesController } from './devices.controller';
|
||||||
import { DevicesService } from './devices.service';
|
import { DevicesService } from './devices.service';
|
||||||
import { Device } from '../entities/device.entity';
|
import { Device } from '../entities/device.entity';
|
||||||
import { InventoryDevice } from '../entities/inventory-device.entity';
|
import { InventoryDevice } from '../entities/inventory-device.entity';
|
||||||
|
import { UserProfile } from '../entities/user-profile.entity';
|
||||||
|
import { PaymentsModule } from '../payments/payments.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Device, InventoryDevice])],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Device, InventoryDevice, UserProfile]),
|
||||||
|
forwardRef(() => PaymentsModule),
|
||||||
|
],
|
||||||
controllers: [DevicesController],
|
controllers: [DevicesController],
|
||||||
providers: [DevicesService],
|
providers: [DevicesService],
|
||||||
exports: [DevicesService],
|
exports: [DevicesService],
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { EventsController } from './events.controller';
|
import { EventsController } from './events.controller';
|
||||||
import { EventsService } from './events.service';
|
import { EventsService } from './events.service';
|
||||||
@ -6,9 +6,14 @@ import { AwsService } from '../aws/aws.service';
|
|||||||
import { RawEvent } from '../entities/raw-event.entity';
|
import { RawEvent } from '../entities/raw-event.entity';
|
||||||
import { Device } from '../entities/device.entity';
|
import { Device } from '../entities/device.entity';
|
||||||
import { ValidatedEvent } from '../entities/validated-event.entity';
|
import { ValidatedEvent } from '../entities/validated-event.entity';
|
||||||
|
import { UserProfile } from '../entities/user-profile.entity';
|
||||||
|
import { PaymentsModule } from '../payments/payments.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([RawEvent, Device, ValidatedEvent])],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([RawEvent, Device, ValidatedEvent, UserProfile]),
|
||||||
|
forwardRef(() => PaymentsModule),
|
||||||
|
],
|
||||||
controllers: [EventsController],
|
controllers: [EventsController],
|
||||||
providers: [EventsService, AwsService],
|
providers: [EventsService, AwsService],
|
||||||
exports: [EventsService, AwsService],
|
exports: [EventsService, AwsService],
|
||||||
|
|||||||
@ -24,27 +24,30 @@ async function bootstrap() {
|
|||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configure raw body parsing for webhook endpoints
|
// fixme: 打开后json反序列化失败
|
||||||
app.use(
|
// // Configure raw body parsing for webhook endpoints
|
||||||
'/api/v1/payments/webhook',
|
// app.use(
|
||||||
json({
|
// '/api/v1/payments/webhook',
|
||||||
verify: (req: any, res, buf) => {
|
// json({
|
||||||
req.rawBody = buf;
|
// verify: (req: any, res, buf) => {
|
||||||
},
|
// req.rawBody = buf;
|
||||||
}),
|
// },
|
||||||
);
|
// }),
|
||||||
|
// );
|
||||||
|
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
whitelist: true,
|
whitelist: true,
|
||||||
forbidNonWhitelisted: true,
|
forbidNonWhitelisted: true,
|
||||||
transform: true,
|
transform: true,
|
||||||
|
skipMissingProperties: false,
|
||||||
|
validationError: { target: false },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Enable CORS for frontend
|
// Enable CORS for frontend
|
||||||
app.enableCors({
|
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,
|
credentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
try {
|
||||||
const provider = this.getProvider('stripe');
|
const provider = this.getProvider('stripe');
|
||||||
const subscription = await provider.getSubscription(
|
const subscription = await provider.getSubscription(
|
||||||
|
|||||||
1494
package-lock.json
generated
1494
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -29,5 +29,8 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"meteor-web-backend",
|
"meteor-web-backend",
|
||||||
"meteor-frontend"
|
"meteor-frontend"
|
||||||
]
|
],
|
||||||
}
|
"dependencies": {
|
||||||
|
"node-fetch": "^2.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user