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 updateEpic = useUpdateEpic();
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const isHydrated = useAuthStore((state) => state.isHydrated);
|
||||
|
||||
const form = useForm<EpicFormValues>({
|
||||
resolver: zodResolver(epicSchema),
|
||||
@@ -71,9 +72,19 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
||||
});
|
||||
|
||||
async function onSubmit(data: EpicFormValues) {
|
||||
console.log('[EpicForm] onSubmit triggered', { data, user: user?.id, projectId, isHydrated });
|
||||
|
||||
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) {
|
||||
toast.error('User not authenticated');
|
||||
console.error('[EpicForm] User not authenticated');
|
||||
toast.error('Please log in to create an epic');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,20 +93,29 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
||||
estimatedHours: data.estimatedHours || undefined,
|
||||
};
|
||||
|
||||
console.log('[EpicForm] Prepared payload', payload);
|
||||
|
||||
if (isEditing) {
|
||||
console.log('[EpicForm] Updating epic', { epicId: epic.id });
|
||||
await updateEpic.mutateAsync({
|
||||
id: epic.id,
|
||||
data: payload,
|
||||
});
|
||||
console.log('[EpicForm] Epic updated successfully');
|
||||
} else {
|
||||
await createEpic.mutateAsync({
|
||||
console.log('[EpicForm] Creating epic', { projectId, createdBy: user.id });
|
||||
const result = await createEpic.mutateAsync({
|
||||
projectId,
|
||||
createdBy: user.id,
|
||||
...payload,
|
||||
});
|
||||
console.log('[EpicForm] Epic created successfully', result);
|
||||
}
|
||||
|
||||
console.log('[EpicForm] Calling onSuccess callback');
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
console.error('[EpicForm] Operation failed', error);
|
||||
const message = error instanceof Error ? error.message : 'Operation failed';
|
||||
toast.error(message);
|
||||
}
|
||||
@@ -112,7 +132,16 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
||||
|
||||
return (
|
||||
<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
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -218,9 +247,9 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
<Button type="submit" disabled={isLoading || !isHydrated}>
|
||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{isEditing ? 'Update Epic' : 'Create Epic'}
|
||||
{!isHydrated ? 'Loading...' : isEditing ? 'Update Epic' : 'Create Epic'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -7,16 +7,17 @@ import { useCurrentUser } from '@/lib/hooks/useAuth';
|
||||
|
||||
export function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const { isAuthenticated, isLoading } = useAuthStore();
|
||||
const { isAuthenticated, isHydrated } = useAuthStore();
|
||||
const { isLoading: isUserLoading } = useCurrentUser();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isUserLoading && !isAuthenticated) {
|
||||
if (isHydrated && !isUserLoading && !isAuthenticated) {
|
||||
console.log('[AuthGuard] Redirecting to login - user not authenticated');
|
||||
router.push('/login');
|
||||
}
|
||||
}, [isAuthenticated, isLoading, isUserLoading, router]);
|
||||
}, [isAuthenticated, isHydrated, isUserLoading, router]);
|
||||
|
||||
if (isLoading || isUserLoading) {
|
||||
if (!isHydrated || isUserLoading) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
|
||||
@@ -49,9 +49,18 @@ apiClient.interceptors.request.use(
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
console.log('[API] Request:', {
|
||||
method: config.method?.toUpperCase(),
|
||||
url: config.url,
|
||||
hasAuth: !!token,
|
||||
data: config.data,
|
||||
});
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
(error) => {
|
||||
console.error('[API] Request interceptor error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor: automatically refresh Token
|
||||
@@ -74,8 +83,22 @@ const processQueue = (error: unknown, token: string | null = null) => {
|
||||
};
|
||||
|
||||
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) => {
|
||||
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 & {
|
||||
_retry?: boolean;
|
||||
};
|
||||
|
||||
@@ -26,7 +26,15 @@ export const epicsApi = {
|
||||
},
|
||||
|
||||
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> => {
|
||||
|
||||
@@ -30,16 +30,20 @@ export function useLogin() {
|
||||
tokenManager.setAccessToken(data.accessToken);
|
||||
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({
|
||||
id: data.user.id,
|
||||
email: data.user.email,
|
||||
fullName: data.user.fullName,
|
||||
tenantId: data.user.tenantId,
|
||||
tenantName: data.user.tenantName,
|
||||
role: data.user.role,
|
||||
isEmailVerified: data.user.isEmailVerified,
|
||||
createdAt: data.user.createdAt || new Date().toISOString(),
|
||||
updatedAt: data.user.updatedAt,
|
||||
id: backendUser.id || backendUser.Id, // Handle both casing
|
||||
email: backendUser.email || backendUser.Email,
|
||||
fullName: backendUser.fullName || backendUser.FullName,
|
||||
tenantId: backendUser.tenantId || backendUser.TenantId,
|
||||
tenantName: data.tenant?.name || data.tenant?.Name || 'Unknown',
|
||||
role: data.tenant?.role || backendUser.role || 'TenantMember',
|
||||
isEmailVerified: backendUser.isEmailVerified ?? backendUser.IsEmailVerified ?? false,
|
||||
createdAt: backendUser.createdAt || backendUser.CreatedAt || new Date().toISOString(),
|
||||
updatedAt: backendUser.updatedAt || backendUser.UpdatedAt,
|
||||
});
|
||||
|
||||
router.push('/dashboard');
|
||||
@@ -95,9 +99,24 @@ export function useCurrentUser() {
|
||||
queryKey: ['currentUser'],
|
||||
queryFn: async () => {
|
||||
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);
|
||||
return data;
|
||||
return mappedUser;
|
||||
},
|
||||
enabled: !!tokenManager.getAccessToken(),
|
||||
retry: false,
|
||||
|
||||
@@ -36,7 +36,8 @@ export class SignalRConnectionManager {
|
||||
|
||||
this.connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl(this.hubUrl, {
|
||||
accessTokenFactory: () => token,
|
||||
// Use dynamic token factory to always get the latest token
|
||||
accessTokenFactory: () => tokenManager.getAccessToken() || '',
|
||||
// 备用方案:使用 query string(WebSocket 升级需要)
|
||||
// transport: signalR.HttpTransportType.WebSockets,
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ interface AuthState {
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
isHydrated: boolean;
|
||||
|
||||
setUser: (user: User) => void;
|
||||
clearUser: () => void;
|
||||
@@ -18,6 +19,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: true,
|
||||
isHydrated: false,
|
||||
|
||||
setUser: (user) =>
|
||||
set({ user, isAuthenticated: true, isLoading: false }),
|
||||
@@ -31,6 +33,38 @@ export const useAuthStore = create<AuthState>()(
|
||||
user: state.user,
|
||||
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