diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index ee697ab8..556b766c 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -65,6 +65,22 @@ export const authOptions: NextAuthOptions = { } try { + // Token has expired, try to refresh it + function isNonEmptyString(value: string | undefined): value is string { + return typeof value === 'string' && value.length > 0; + } + + const clientId = process.env.KEYCLOAK_CLIENT_ID; + const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET; + + if (!isNonEmptyString(clientId) || !isNonEmptyString(clientSecret)) { + throw new Error("Missing required environment variables for token refresh"); + } + + // After the type guard check, we can safely assert these as strings + const validClientId = clientId as string; + const validClientSecret = clientSecret as string; + const response = await fetch( `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, { @@ -74,8 +90,8 @@ export const authOptions: NextAuthOptions = { }, body: new URLSearchParams({ grant_type: "refresh_token", - client_id: process.env.KEYCLOAK_CLIENT_ID!, - client_secret: process.env.KEYCLOAK_CLIENT_SECRET!, + client_id: validClientId, + client_secret: validClientSecret, refresh_token: token.refreshToken as string, }), } @@ -83,7 +99,10 @@ export const authOptions: NextAuthOptions = { const tokens = await response.json(); - if (!response.ok) throw tokens; + if (!response.ok) { + console.error("Token refresh failed:", tokens); + throw new Error("RefreshAccessTokenError"); + } return { ...token, @@ -93,12 +112,18 @@ export const authOptions: NextAuthOptions = { }; } catch (error) { console.error("Error refreshing token:", error); - return { ...token, error: "RefreshAccessTokenError" }; + + // Return token with error flag - this will trigger a redirect to sign-in + return { + ...token, + error: "RefreshAccessTokenError", + }; } }, async session({ session, token }) { if (token.error) { + // Force sign out if there was a refresh error throw new Error("RefreshAccessTokenError"); } diff --git a/components/flow.tsx b/components/flow.tsx index 57a22a23..813aee73 100644 --- a/components/flow.tsx +++ b/components/flow.tsx @@ -5,57 +5,52 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { RefreshCw } from "lucide-react"; -interface StatusLabel { +interface Task { id: string; - name: string; - class: string; - statusType: string; - kanbanCol: boolean | string; - sortKey: number | string; -} - -interface Project { - id: number; - name: string; - labels: StatusLabel[]; + headline: string; + projectName: string; + projectId: number; + status: string; + dueDate: string | null; + milestone?: string; } export function Flow() { - const [projects, setProjects] = useState([]); + const [tasks, setTasks] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [retryTimeout, setRetryTimeout] = useState(null); - const fetchStatusLabels = async (isRefresh = false) => { + const fetchTasks = async (isRefresh = false) => { try { if (isRefresh) { setRefreshing(true); } - const response = await fetch('/api/leantime/status-labels'); + const response = await fetch('/api/leantime/tasks'); if (response.status === 429) { const retryAfter = parseInt(response.headers.get('retry-after') || '60'); - const timeout = setTimeout(() => fetchStatusLabels(), retryAfter * 1000); + const timeout = setTimeout(() => fetchTasks(), retryAfter * 1000); setRetryTimeout(timeout); setError(`Rate limit exceeded. Retrying in ${retryAfter} seconds...`); return; } if (!response.ok) { - throw new Error('Failed to fetch status labels'); + throw new Error('Failed to fetch tasks'); } const data = await response.json(); - if (data.projects && Array.isArray(data.projects)) { - setProjects(data.projects); + if (data.tasks && Array.isArray(data.tasks)) { + setTasks(data.tasks); } else { - setProjects([]); + setTasks([]); } setError(null); } catch (err) { - console.error('Error fetching status labels:', err); - setError('Failed to fetch status labels'); + console.error('Error fetching tasks:', err); + setError('Failed to fetch tasks'); } finally { setLoading(false); setRefreshing(false); @@ -63,7 +58,7 @@ export function Flow() { }; useEffect(() => { - fetchStatusLabels(); + fetchTasks(); return () => { if (retryTimeout) { clearTimeout(retryTimeout); @@ -71,6 +66,38 @@ export function Flow() { }; }, []); + // Group tasks by project + const tasksByProject = tasks.reduce((acc, task) => { + if (!acc[task.projectName]) { + acc[task.projectName] = []; + } + acc[task.projectName].push(task); + return acc; + }, {} as Record); + + const getStatusClass = (status: string): string => { + switch (status.toLowerCase()) { + case 'new': + return 'bg-blue-100 text-blue-800'; + case 'in_progress': + return 'bg-yellow-100 text-yellow-800'; + case 'completed': + return 'bg-green-100 text-green-800'; + default: + return 'bg-gray-100 text-gray-800'; + } + }; + + const formatDate = (dateString: string | null): string => { + if (!dateString) return 'No due date'; + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: '2-digit', + day: '2-digit', + year: 'numeric' + }); + }; + return ( @@ -78,7 +105,7 @@ export function Flow() {