Compare commits
6 Commits
a019479381
...
d9228057bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9228057bb | ||
|
|
605e151f33 | ||
|
|
6f36bbc3d5 | ||
|
|
be69325797 | ||
|
|
b404fbb006 | ||
|
|
048e7e7e6d |
@@ -59,6 +59,7 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
|||||||
const createEpic = useCreateEpic();
|
const createEpic = useCreateEpic();
|
||||||
const updateEpic = useUpdateEpic();
|
const updateEpic = useUpdateEpic();
|
||||||
const user = useAuthStore((state) => state.user);
|
const user = useAuthStore((state) => state.user);
|
||||||
|
const isHydrated = useAuthStore((state) => state.isHydrated);
|
||||||
|
|
||||||
const form = useForm<EpicFormValues>({
|
const form = useForm<EpicFormValues>({
|
||||||
resolver: zodResolver(epicSchema),
|
resolver: zodResolver(epicSchema),
|
||||||
@@ -71,9 +72,19 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(data: EpicFormValues) {
|
async function onSubmit(data: EpicFormValues) {
|
||||||
|
console.log('[EpicForm] onSubmit triggered', { data, user: user?.id, projectId, isHydrated });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if auth store has completed hydration
|
||||||
|
if (!isHydrated) {
|
||||||
|
console.warn('[EpicForm] Auth store not hydrated yet, waiting...');
|
||||||
|
toast.error('Loading user information, please try again in a moment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
toast.error('User not authenticated');
|
console.error('[EpicForm] User not authenticated');
|
||||||
|
toast.error('Please log in to create an epic');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,20 +93,29 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
|||||||
estimatedHours: data.estimatedHours || undefined,
|
estimatedHours: data.estimatedHours || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('[EpicForm] Prepared payload', payload);
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
|
console.log('[EpicForm] Updating epic', { epicId: epic.id });
|
||||||
await updateEpic.mutateAsync({
|
await updateEpic.mutateAsync({
|
||||||
id: epic.id,
|
id: epic.id,
|
||||||
data: payload,
|
data: payload,
|
||||||
});
|
});
|
||||||
|
console.log('[EpicForm] Epic updated successfully');
|
||||||
} else {
|
} else {
|
||||||
await createEpic.mutateAsync({
|
console.log('[EpicForm] Creating epic', { projectId, createdBy: user.id });
|
||||||
|
const result = await createEpic.mutateAsync({
|
||||||
projectId,
|
projectId,
|
||||||
createdBy: user.id,
|
createdBy: user.id,
|
||||||
...payload,
|
...payload,
|
||||||
});
|
});
|
||||||
|
console.log('[EpicForm] Epic created successfully', result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[EpicForm] Calling onSuccess callback');
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[EpicForm] Operation failed', error);
|
||||||
const message = error instanceof Error ? error.message : 'Operation failed';
|
const message = error instanceof Error ? error.message : 'Operation failed';
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
}
|
}
|
||||||
@@ -112,7 +132,16 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
console.log('[EpicForm] Form submit event triggered', {
|
||||||
|
formState: form.formState,
|
||||||
|
values: form.getValues(),
|
||||||
|
errors: form.formState.errors,
|
||||||
|
});
|
||||||
|
form.handleSubmit(onSubmit)(e);
|
||||||
|
}}
|
||||||
|
className="space-y-6">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -218,9 +247,9 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" disabled={isLoading}>
|
<Button type="submit" disabled={isLoading || !isHydrated}>
|
||||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
{isEditing ? 'Update Epic' : 'Create Epic'}
|
{!isHydrated ? 'Loading...' : isEditing ? 'Update Epic' : 'Create Epic'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -7,16 +7,17 @@ import { useCurrentUser } from '@/lib/hooks/useAuth';
|
|||||||
|
|
||||||
export function AuthGuard({ children }: { children: React.ReactNode }) {
|
export function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isAuthenticated, isLoading } = useAuthStore();
|
const { isAuthenticated, isHydrated } = useAuthStore();
|
||||||
const { isLoading: isUserLoading } = useCurrentUser();
|
const { isLoading: isUserLoading } = useCurrentUser();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && !isUserLoading && !isAuthenticated) {
|
if (isHydrated && !isUserLoading && !isAuthenticated) {
|
||||||
|
console.log('[AuthGuard] Redirecting to login - user not authenticated');
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, isLoading, isUserLoading, router]);
|
}, [isAuthenticated, isHydrated, isUserLoading, router]);
|
||||||
|
|
||||||
if (isLoading || isUserLoading) {
|
if (!isHydrated || isUserLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center">
|
<div className="flex min-h-screen items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|||||||
@@ -49,9 +49,18 @@ apiClient.interceptors.request.use(
|
|||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
console.log('[API] Request:', {
|
||||||
|
method: config.method?.toUpperCase(),
|
||||||
|
url: config.url,
|
||||||
|
hasAuth: !!token,
|
||||||
|
data: config.data,
|
||||||
|
});
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => Promise.reject(error)
|
(error) => {
|
||||||
|
console.error('[API] Request interceptor error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Response interceptor: automatically refresh Token
|
// Response interceptor: automatically refresh Token
|
||||||
@@ -74,8 +83,22 @@ const processQueue = (error: unknown, token: string | null = null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
apiClient.interceptors.response.use(
|
apiClient.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => {
|
||||||
|
console.log('[API] Response:', {
|
||||||
|
status: response.status,
|
||||||
|
url: response.config.url,
|
||||||
|
data: response.data,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
async (error: AxiosError) => {
|
async (error: AxiosError) => {
|
||||||
|
console.error('[API] Response error:', {
|
||||||
|
status: error.response?.status,
|
||||||
|
url: error.config?.url,
|
||||||
|
message: error.message,
|
||||||
|
data: error.response?.data,
|
||||||
|
});
|
||||||
|
|
||||||
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
||||||
_retry?: boolean;
|
_retry?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,15 @@ export const epicsApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
create: async (data: CreateEpicDto): Promise<Epic> => {
|
create: async (data: CreateEpicDto): Promise<Epic> => {
|
||||||
return api.post('/api/v1/epics', data);
|
console.log('[epicsApi.create] Sending request', { url: '/api/v1/epics', data });
|
||||||
|
try {
|
||||||
|
const result = await api.post('/api/v1/epics', data);
|
||||||
|
console.log('[epicsApi.create] Request successful', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[epicsApi.create] Request failed', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (id: string, data: UpdateEpicDto): Promise<Epic> => {
|
update: async (id: string, data: UpdateEpicDto): Promise<Epic> => {
|
||||||
|
|||||||
@@ -30,16 +30,20 @@ export function useLogin() {
|
|||||||
tokenManager.setAccessToken(data.accessToken);
|
tokenManager.setAccessToken(data.accessToken);
|
||||||
tokenManager.setRefreshToken(data.refreshToken);
|
tokenManager.setRefreshToken(data.refreshToken);
|
||||||
|
|
||||||
|
// Map backend field names to frontend User type
|
||||||
|
// Backend returns: { Id, TenantId, Email, FullName, ... }
|
||||||
|
// Frontend expects: { id, tenantId, email, fullName, ... }
|
||||||
|
const backendUser = data.user;
|
||||||
setUser({
|
setUser({
|
||||||
id: data.user.id,
|
id: backendUser.id || backendUser.Id, // Handle both casing
|
||||||
email: data.user.email,
|
email: backendUser.email || backendUser.Email,
|
||||||
fullName: data.user.fullName,
|
fullName: backendUser.fullName || backendUser.FullName,
|
||||||
tenantId: data.user.tenantId,
|
tenantId: backendUser.tenantId || backendUser.TenantId,
|
||||||
tenantName: data.user.tenantName,
|
tenantName: data.tenant?.name || data.tenant?.Name || 'Unknown',
|
||||||
role: data.user.role,
|
role: data.tenant?.role || backendUser.role || 'TenantMember',
|
||||||
isEmailVerified: data.user.isEmailVerified,
|
isEmailVerified: backendUser.isEmailVerified ?? backendUser.IsEmailVerified ?? false,
|
||||||
createdAt: data.user.createdAt || new Date().toISOString(),
|
createdAt: backendUser.createdAt || backendUser.CreatedAt || new Date().toISOString(),
|
||||||
updatedAt: data.user.updatedAt,
|
updatedAt: backendUser.updatedAt || backendUser.UpdatedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
@@ -95,9 +99,24 @@ export function useCurrentUser() {
|
|||||||
queryKey: ['currentUser'],
|
queryKey: ['currentUser'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiClient.get(API_ENDPOINTS.ME);
|
const { data } = await apiClient.get(API_ENDPOINTS.ME);
|
||||||
setUser(data);
|
|
||||||
|
// Map backend /me response to frontend User type
|
||||||
|
// Backend returns: { userId, tenantId, email, fullName, tenantSlug, tenantRole, role }
|
||||||
|
// Frontend expects: { id, tenantId, email, fullName, tenantName, role, isEmailVerified, createdAt }
|
||||||
|
const mappedUser = {
|
||||||
|
id: data.userId || data.id, // Backend uses 'userId'
|
||||||
|
email: data.email,
|
||||||
|
fullName: data.fullName,
|
||||||
|
tenantId: data.tenantId,
|
||||||
|
tenantName: data.tenantSlug || 'Unknown', // Use tenantSlug as tenantName fallback
|
||||||
|
role: data.tenantRole || data.role || 'TenantMember',
|
||||||
|
isEmailVerified: true, // Assume verified if token is valid
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setUser(mappedUser);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return data;
|
return mappedUser;
|
||||||
},
|
},
|
||||||
enabled: !!tokenManager.getAccessToken(),
|
enabled: !!tokenManager.getAccessToken(),
|
||||||
retry: false,
|
retry: false,
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export class SignalRConnectionManager {
|
|||||||
|
|
||||||
this.connection = new signalR.HubConnectionBuilder()
|
this.connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl(this.hubUrl, {
|
.withUrl(this.hubUrl, {
|
||||||
accessTokenFactory: () => token,
|
// Use dynamic token factory to always get the latest token
|
||||||
|
accessTokenFactory: () => tokenManager.getAccessToken() || '',
|
||||||
// 备用方案:使用 query string(WebSocket 升级需要)
|
// 备用方案:使用 query string(WebSocket 升级需要)
|
||||||
// transport: signalR.HttpTransportType.WebSockets,
|
// transport: signalR.HttpTransportType.WebSockets,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface AuthState {
|
|||||||
user: User | null;
|
user: User | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isHydrated: boolean;
|
||||||
|
|
||||||
setUser: (user: User) => void;
|
setUser: (user: User) => void;
|
||||||
clearUser: () => void;
|
clearUser: () => void;
|
||||||
@@ -18,6 +19,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
user: null,
|
user: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
isHydrated: false,
|
||||||
|
|
||||||
setUser: (user) =>
|
setUser: (user) =>
|
||||||
set({ user, isAuthenticated: true, isLoading: false }),
|
set({ user, isAuthenticated: true, isLoading: false }),
|
||||||
@@ -31,6 +33,38 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
user: state.user,
|
user: state.user,
|
||||||
isAuthenticated: state.isAuthenticated,
|
isAuthenticated: state.isAuthenticated,
|
||||||
}),
|
}),
|
||||||
|
// 数据迁移函数:将旧格式的 userId 转换为新格式的 id
|
||||||
|
migrate: (persistedState: any, version: number) => {
|
||||||
|
console.log('[AuthStore] Migrating persisted state', { version, persistedState });
|
||||||
|
|
||||||
|
// 如果存在旧的 userId 字段,迁移到 id
|
||||||
|
if (persistedState?.user?.userId && !persistedState?.user?.id) {
|
||||||
|
console.log('[AuthStore] Migrating userId to id');
|
||||||
|
persistedState.user.id = persistedState.user.userId;
|
||||||
|
delete persistedState.user.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedState;
|
||||||
|
},
|
||||||
|
onRehydrateStorage: () => (state) => {
|
||||||
|
console.log('[AuthStore] Hydration started');
|
||||||
|
if (state) {
|
||||||
|
// 额外的安全检查:确保 user 对象有 id 字段
|
||||||
|
if (state.user && (state.user as any).userId && !state.user.id) {
|
||||||
|
console.log('[AuthStore] Post-hydration migration: userId -> id');
|
||||||
|
state.user.id = (state.user as any).userId;
|
||||||
|
delete (state.user as any).userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isHydrated = true;
|
||||||
|
state.isLoading = false; // 水合完成后停止 loading
|
||||||
|
console.log('[AuthStore] Hydration completed', {
|
||||||
|
userId: state.user?.id,
|
||||||
|
isAuthenticated: state.isAuthenticated,
|
||||||
|
isLoading: state.isLoading
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user