diff --git a/AGENT.md b/AGENT.md index 5ee040d3..36e8d36d 100644 --- a/AGENT.md +++ b/AGENT.md @@ -61,6 +61,10 @@ ts-rs allows you to derive TypeScript types from Rust structs/enums. By annotati When making changes to the types, you can regenerate them using `npm run generate-types` Do not manually edit shared/types.ts -# Process +# Working on the frontend AND the backend When working on any task that involves changes to the backend and the frontend, start with the backend. If any shared types need to be regenerated, regenerate them before starting the frontend changes. + +# Testing your work + +Try to build the Typescript project after any frontend changes `npm run build` diff --git a/backend/src/auth.rs b/backend/src/auth.rs index 61a7426d..68a9711f 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -1,10 +1,14 @@ use axum::{ async_trait, + body::Body, extract::FromRequestParts, - http::{request::Parts, StatusCode}, + http::{request::Parts, StatusCode, Request}, + middleware::Next, + response::Response, }; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; +use sqlx::PgPool; use uuid::Uuid; #[derive(Debug, Serialize, Deserialize)] @@ -15,6 +19,7 @@ pub struct Claims { pub exp: usize, } +#[derive(Clone)] pub struct AuthUser { pub user_id: Uuid, pub email: String, @@ -29,32 +34,12 @@ where type Rejection = StatusCode; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - let headers = &parts.headers; - - let auth_header = headers - .get("authorization") - .and_then(|value| value.to_str().ok()) - .ok_or(StatusCode::UNAUTHORIZED)?; - - let token = auth_header - .strip_prefix("Bearer ") - .ok_or(StatusCode::UNAUTHORIZED)?; - - let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your-secret-key".to_string()); - - let claims = decode::( - token, - &DecodingKey::from_secret(jwt_secret.as_ref()), - &Validation::default(), - ) - .map_err(|_| StatusCode::UNAUTHORIZED)? - .claims; - - Ok(AuthUser { - user_id: claims.user_id, - email: claims.email, - is_admin: claims.is_admin, - }) + // Get user from request extensions (set by auth middleware) + parts + .extensions + .get::() + .cloned() + .ok_or(StatusCode::UNAUTHORIZED) } } @@ -87,3 +72,58 @@ pub fn hash_password(password: &str) -> Result { pub fn verify_password(password: &str, hash: &str) -> Result { bcrypt::verify(password, hash) } + +// Auth middleware that requires authentication for all routes +pub async fn auth_middleware( + mut request: Request, + next: Next, +) -> Result { + let headers = request.headers(); + + let auth_header = headers + .get("authorization") + .and_then(|value| value.to_str().ok()) + .ok_or(StatusCode::UNAUTHORIZED)?; + + let token = auth_header + .strip_prefix("Bearer ") + .ok_or(StatusCode::UNAUTHORIZED)?; + + let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your-secret-key".to_string()); + + let claims = decode::( + token, + &DecodingKey::from_secret(jwt_secret.as_ref()), + &Validation::default(), + ) + .map_err(|_| StatusCode::UNAUTHORIZED)? + .claims; + + // Get database pool from request extensions + let pool = request + .extensions() + .get::() + .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; + + // Verify user exists in database + let user_exists = sqlx::query!( + "SELECT id FROM users WHERE id = $1", + claims.user_id + ) + .fetch_optional(pool) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + if user_exists.is_none() { + return Err(StatusCode::UNAUTHORIZED); + } + + // Add user info to request extensions for handlers to access + request.extensions_mut().insert(AuthUser { + user_id: claims.user_id, + email: claims.email, + is_admin: claims.is_admin, + }); + + Ok(next.run(request).await) +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 23d1e0b9..91f64178 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,5 +1,6 @@ use axum::{ extract::Extension, + middleware, response::Json as ResponseJson, routing::{get, post}, Json, Router, @@ -12,7 +13,7 @@ mod auth; mod models; mod routes; -use auth::hash_password; +use auth::{auth_middleware, hash_password}; use models::ApiResponse; use routes::{health, projects, tasks, users}; @@ -51,13 +52,24 @@ async fn main() -> anyhow::Result<()> { tracing::warn!("Failed to create admin account: {}", e); } - let app = Router::new() + // Public routes (no auth required) + let public_routes = Router::new() .route("/", get(|| async { "Bloop API" })) .route("/health", get(health::health_check)) .route("/echo", post(echo_handler)) + .merge(users::public_users_router()); + + // Protected routes (auth required) + let protected_routes = Router::new() .merge(projects::projects_router()) .merge(tasks::tasks_router()) - .merge(users::users_router()) + .merge(users::protected_users_router()) + .layer(Extension(pool.clone())) + .layer(middleware::from_fn(auth_middleware)); + + let app = Router::new() + .merge(public_routes) + .merge(protected_routes) .layer(Extension(pool)) .layer(CorsLayer::permissive()); diff --git a/backend/src/routes/projects.rs b/backend/src/routes/projects.rs index a8113873..9ff1f757 100644 --- a/backend/src/routes/projects.rs +++ b/backend/src/routes/projects.rs @@ -13,7 +13,10 @@ use chrono::Utc; use crate::models::{ApiResponse, project::{Project, CreateProject, UpdateProject}}; use crate::auth::AuthUser; -pub async fn get_projects(Extension(pool): Extension) -> Result>>, StatusCode> { +pub async fn get_projects( + auth: AuthUser, + Extension(pool): Extension +) -> Result>>, StatusCode> { match sqlx::query_as!( Project, "SELECT id, name, owner_id, created_at, updated_at FROM projects ORDER BY created_at DESC" @@ -34,6 +37,7 @@ pub async fn get_projects(Extension(pool): Extension) -> Result, Extension(pool): Extension ) -> Result>, StatusCode> { diff --git a/backend/src/routes/tasks.rs b/backend/src/routes/tasks.rs index 1c377aaf..d8d5c7f6 100644 --- a/backend/src/routes/tasks.rs +++ b/backend/src/routes/tasks.rs @@ -14,6 +14,7 @@ use crate::models::{ApiResponse, task::{Task, CreateTask, UpdateTask, TaskStatus use crate::auth::AuthUser; pub async fn get_project_tasks( + auth: AuthUser, Path(project_id): Path, Extension(pool): Extension ) -> Result>>, StatusCode> { @@ -41,6 +42,7 @@ pub async fn get_project_tasks( } pub async fn get_task( + auth: AuthUser, Path((project_id, task_id)): Path<(Uuid, Uuid)>, Extension(pool): Extension ) -> Result>, StatusCode> { diff --git a/backend/src/routes/users.rs b/backend/src/routes/users.rs index d1fd82dc..5721a917 100644 --- a/backend/src/routes/users.rs +++ b/backend/src/routes/users.rs @@ -292,9 +292,29 @@ pub async fn get_current_user( } } -pub fn users_router() -> Router { +pub async fn check_auth_status( + auth: AuthUser, +) -> ResponseJson> { + ResponseJson(ApiResponse { + success: true, + data: Some(serde_json::json!({ + "authenticated": true, + "user_id": auth.user_id, + "email": auth.email, + "is_admin": auth.is_admin + })), + message: Some("User is authenticated".to_string()), + }) +} + +pub fn public_users_router() -> Router { Router::new() .route("/auth/login", post(login)) +} + +pub fn protected_users_router() -> Router { + Router::new() + .route("/auth/status", get(check_auth_status)) .route("/auth/me", get(get_current_user)) .route("/users", get(get_users).post(create_user)) .route("/users/:id", get(get_user).put(update_user).delete(delete_user)) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 29db7181..49c6dd06 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,3 @@ -import { useState, useEffect } from 'react' import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom' import { LoginForm } from '@/components/auth/login-form' import { Navbar } from '@/components/layout/navbar' @@ -6,32 +5,37 @@ import { HomePage } from '@/pages/home' import { Projects } from '@/pages/projects' import { ProjectTasks } from '@/pages/project-tasks' import { Users } from '@/pages/users' -import { isAuthenticated } from '@/lib/auth' +import { AuthProvider, useAuth } from '@/contexts/auth-context' function AppContent() { const location = useLocation() - const [authenticated, setAuthenticated] = useState(false) - const showNavbar = location.pathname !== '/' || authenticated - - useEffect(() => { - setAuthenticated(isAuthenticated()) - }, []) + const { isAuthenticated, isLoading, logout } = useAuth() + const showNavbar = location.pathname !== '/' || isAuthenticated const handleLogin = () => { - setAuthenticated(true) + // The actual login logic is handled by the LoginForm component + // which will call the login method from useAuth() } - const handleLogout = () => { - setAuthenticated(false) + // Show loading while checking auth status + if (isLoading) { + return ( +
+
+
+

Checking authentication...

+
+
+ ) } - if (!authenticated) { + if (!isAuthenticated) { return } return (
- {showNavbar && } + {showNavbar && }
} /> @@ -48,7 +52,9 @@ function AppContent() { function App() { return ( - + + + ) } diff --git a/frontend/src/components/auth/login-form.tsx b/frontend/src/components/auth/login-form.tsx index 093d2f35..c0990315 100644 --- a/frontend/src/components/auth/login-form.tsx +++ b/frontend/src/components/auth/login-form.tsx @@ -4,15 +4,16 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Alert, AlertDescription } from '@/components/ui/alert' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { LoginRequest, LoginResponse, ApiResponse } from 'shared/types' -import { authStorage } from '@/lib/auth' +import { LoginRequest, LoginResponse, ApiResponse } from '@/types' +import { useAuth } from '@/contexts/auth-context' import { LogIn, AlertCircle } from 'lucide-react' interface LoginFormProps { - onSuccess: () => void + onSuccess?: () => void } export function LoginForm({ onSuccess }: LoginFormProps) { + const { login } = useAuth() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [loading, setLoading] = useState(false) @@ -41,9 +42,8 @@ export function LoginForm({ onSuccess }: LoginFormProps) { const data: ApiResponse = await response.json() if (data.success && data.data) { - authStorage.setToken(data.data.token) - authStorage.setUser(data.data.user) - onSuccess() + login(data.data.user, data.data.token) + onSuccess?.() } else { throw new Error('Login failed') } diff --git a/frontend/src/components/layout/navbar.tsx b/frontend/src/components/layout/navbar.tsx index f890b855..14445b99 100644 --- a/frontend/src/components/layout/navbar.tsx +++ b/frontend/src/components/layout/navbar.tsx @@ -1,6 +1,6 @@ import { Link, useLocation } from 'react-router-dom' import { Button } from '@/components/ui/button' -import { authStorage, logout } from '@/lib/auth' +import { authStorage } from '@/lib/auth' import { ArrowLeft, FolderOpen, Users, LogOut } from 'lucide-react' interface NavbarProps { @@ -13,7 +13,6 @@ export function Navbar({ onLogout }: NavbarProps) { const isHome = location.pathname === '/' const handleLogout = () => { - logout() onLogout() } diff --git a/frontend/src/components/projects/project-detail.tsx b/frontend/src/components/projects/project-detail.tsx index 8f037598..d05dfa65 100644 --- a/frontend/src/components/projects/project-detail.tsx +++ b/frontend/src/components/projects/project-detail.tsx @@ -4,8 +4,9 @@ import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Alert, AlertDescription } from '@/components/ui/alert' -import { Project, ApiResponse } from 'shared/types' +import { Project, ApiResponse } from '@/types' import { ProjectForm } from './project-form' +import { makeAuthenticatedRequest } from '@/lib/auth' import { ArrowLeft, Edit, Trash2, Calendar, Clock, User, AlertCircle, Loader2, CheckSquare } from 'lucide-react' interface ProjectDetailProps { @@ -24,7 +25,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) { setLoading(true) setError('') try { - const response = await fetch(`/api/projects/${projectId}`) + const response = await makeAuthenticatedRequest(`/api/projects/${projectId}`) const data: ApiResponse = await response.json() if (data.success && data.data) { setProject(data.data) @@ -44,7 +45,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) { if (!confirm(`Are you sure you want to delete "${project.name}"? This action cannot be undone.`)) return try { - const response = await fetch(`/api/projects/${projectId}`, { + const response = await makeAuthenticatedRequest(`/api/projects/${projectId}`, { method: 'DELETE', }) if (response.ok) { diff --git a/frontend/src/components/projects/project-form.tsx b/frontend/src/components/projects/project-form.tsx index 091a9870..d38d5b08 100644 --- a/frontend/src/components/projects/project-form.tsx +++ b/frontend/src/components/projects/project-form.tsx @@ -4,7 +4,7 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Alert, AlertDescription } from '@/components/ui/alert' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { Project, CreateProject, UpdateProject } from 'shared/types' +import { Project, CreateProject, UpdateProject } from '@/types' import { AlertCircle } from 'lucide-react' import { makeAuthenticatedRequest } from '@/lib/auth' diff --git a/frontend/src/components/projects/project-list.tsx b/frontend/src/components/projects/project-list.tsx index 23a9854c..7e3e82bd 100644 --- a/frontend/src/components/projects/project-list.tsx +++ b/frontend/src/components/projects/project-list.tsx @@ -4,8 +4,9 @@ import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Alert, AlertDescription } from '@/components/ui/alert' -import { Project, ApiResponse } from 'shared/types' +import { Project, ApiResponse } from '@/types' import { ProjectForm } from './project-form' +import { makeAuthenticatedRequest } from '@/lib/auth' import { Plus, Edit, Trash2, Calendar, AlertCircle, Loader2, CheckSquare } from 'lucide-react' export function ProjectList() { @@ -20,7 +21,7 @@ export function ProjectList() { setLoading(true) setError('') try { - const response = await fetch('/api/projects') + const response = await makeAuthenticatedRequest('/api/projects') const data: ApiResponse = await response.json() if (data.success && data.data) { setProjects(data.data) @@ -39,7 +40,7 @@ export function ProjectList() { if (!confirm(`Are you sure you want to delete "${name}"? This action cannot be undone.`)) return try { - const response = await fetch(`/api/projects/${id}`, { + const response = await makeAuthenticatedRequest(`/api/projects/${id}`, { method: 'DELETE', }) if (response.ok) { diff --git a/frontend/src/components/users/user-form.tsx b/frontend/src/components/users/user-form.tsx index ff4dfa9f..0834f347 100644 --- a/frontend/src/components/users/user-form.tsx +++ b/frontend/src/components/users/user-form.tsx @@ -4,7 +4,7 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Alert, AlertDescription } from '@/components/ui/alert' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { User, CreateUser, UpdateUser } from 'shared/types' +import { User, CreateUser, UpdateUser } from '@/types' import { makeAuthenticatedRequest, authStorage } from '@/lib/auth' import { AlertCircle } from 'lucide-react' diff --git a/frontend/src/components/users/user-list.tsx b/frontend/src/components/users/user-list.tsx index f8c52c6f..b573ce2c 100644 --- a/frontend/src/components/users/user-list.tsx +++ b/frontend/src/components/users/user-list.tsx @@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Alert, AlertDescription } from '@/components/ui/alert' -import { User, ApiResponse } from 'shared/types' +import { User, ApiResponse } from '@/types' import { UserForm } from './user-form' import { makeAuthenticatedRequest, authStorage } from '@/lib/auth' import { Plus, Edit, Trash2, Calendar, AlertCircle, Loader2, Shield, User as UserIcon } from 'lucide-react' @@ -142,7 +142,7 @@ export function UserList() {
- Joined {new Date(user.created_at).toLocaleDateString()} + Joined {user.created_at ? new Date(user.created_at).toLocaleDateString() : 'Unknown'} diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx new file mode 100644 index 00000000..7244034f --- /dev/null +++ b/frontend/src/contexts/auth-context.tsx @@ -0,0 +1,120 @@ +import { createContext, useContext, useState, useEffect, ReactNode } from 'react' +import { isAuthenticated, authStorage, makeAuthenticatedRequest } from '@/lib/auth' +import { User } from '@/types' + +interface AuthContextType { + user: User | null + isAuthenticated: boolean + isLoading: boolean + login: (user: User, token: string) => void + logout: () => void + refreshAuthStatus: () => Promise +} + +const AuthContext = createContext(undefined) + +interface AuthProviderProps { + children: ReactNode +} + +export function AuthProvider({ children }: AuthProviderProps) { + const [user, setUser] = useState(null) + const [isAuthenticatedState, setIsAuthenticated] = useState(false) + const [isLoading, setIsLoading] = useState(true) + + const checkAuthStatus = async (): Promise => { + const token = authStorage.getToken() + if (!token) { + return false + } + + try { + const response = await makeAuthenticatedRequest('/api/auth/status') + + if (response.ok) { + const data = await response.json() + if (data.success && data.data?.authenticated) { + // Update user data from server response + if (data.data.user_id && data.data.email) { + const userData: User = { + id: data.data.user_id, + email: data.data.email, + is_admin: data.data.is_admin || false + } + authStorage.setUser(userData) + setUser(userData) + } + return true + } + } + + // If we get here, the token is invalid + return false + } catch (error) { + console.error('Auth status check failed:', error) + return false + } + } + + const refreshAuthStatus = async () => { + setIsLoading(true) + + if (isAuthenticated()) { + const isValid = await checkAuthStatus() + if (isValid) { + setIsAuthenticated(true) + } else { + // Clear invalid auth state + authStorage.clear() + setUser(null) + setIsAuthenticated(false) + } + } else { + setUser(null) + setIsAuthenticated(false) + } + + setIsLoading(false) + } + + useEffect(() => { + refreshAuthStatus() + }, []) + + const login = (userData: User, token: string) => { + authStorage.setToken(token) + authStorage.setUser(userData) + setUser(userData) + setIsAuthenticated(true) + } + + const logout = () => { + authStorage.clear() + setUser(null) + setIsAuthenticated(false) + window.location.href = '/' + } + + const value: AuthContextType = { + user, + isAuthenticated: isAuthenticatedState, + isLoading, + login, + logout, + refreshAuthStatus + } + + return ( + + {children} + + ) +} + +export function useAuth() { + const context = useContext(AuthContext) + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 8fb71e5b..e7a6ef6c 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -1,4 +1,4 @@ -import { User } from 'shared/types' +import { User } from '@/types' const TOKEN_KEY = 'auth_token' const USER_KEY = 'auth_user' @@ -56,8 +56,3 @@ export const makeAuthenticatedRequest = async (url: string, options: RequestInit export const isAuthenticated = (): boolean => { return !!authStorage.getToken() } - -export const logout = (): void => { - authStorage.clear() - window.location.href = '/' -} diff --git a/frontend/src/pages/home.tsx b/frontend/src/pages/home.tsx index c048b77a..d1c1d272 100644 --- a/frontend/src/pages/home.tsx +++ b/frontend/src/pages/home.tsx @@ -10,8 +10,7 @@ import { } from "@/components/ui/card"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; -import { Separator } from "@/components/ui/separator"; -import { ApiResponse } from "shared/types"; +import { ApiResponse } from "@/types"; import { authStorage, makeAuthenticatedRequest } from "@/lib/auth"; import { Heart, @@ -22,7 +21,6 @@ import { AlertCircle, Zap, Shield, - Code, } from "lucide-react"; export function HomePage() { diff --git a/frontend/src/pages/project-tasks.tsx b/frontend/src/pages/project-tasks.tsx index 125ea705..bcfece24 100644 --- a/frontend/src/pages/project-tasks.tsx +++ b/frontend/src/pages/project-tasks.tsx @@ -25,7 +25,7 @@ import { SelectValue } from '@/components/ui/select' import { ArrowLeft, Plus, MoreHorizontal, Trash2, Edit } from 'lucide-react' -import { getAuthHeaders } from '@/lib/auth' +import { makeAuthenticatedRequest } from '@/lib/auth' import { KanbanProvider, KanbanBoard, @@ -34,7 +34,7 @@ import { KanbanCard, type DragEndEvent } from '@/components/ui/shadcn-io/kanban' -import type { TaskStatus } from '../../../shared/types' +import type { TaskStatus } from '@/types' interface Task { id: string @@ -108,9 +108,7 @@ export function ProjectTasks() { const fetchProject = async () => { try { - const response = await fetch(`/api/projects/${projectId}`, { - headers: getAuthHeaders() - }) + const response = await makeAuthenticatedRequest(`/api/projects/${projectId}`) if (response.ok) { const result: ApiResponse = await response.json() @@ -129,9 +127,7 @@ export function ProjectTasks() { const fetchTasks = async () => { try { setLoading(true) - const response = await fetch(`/api/projects/${projectId}/tasks`, { - headers: getAuthHeaders() - }) + const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks`) if (response.ok) { const result: ApiResponse = await response.json() @@ -152,12 +148,8 @@ export function ProjectTasks() { if (!newTaskTitle.trim()) return try { - const response = await fetch(`/api/projects/${projectId}/tasks`, { + const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks`, { method: 'POST', - headers: { - ...getAuthHeaders(), - 'Content-Type': 'application/json' - }, body: JSON.stringify({ project_id: projectId, title: newTaskTitle, @@ -182,12 +174,8 @@ export function ProjectTasks() { if (!editingTask || !editTaskTitle.trim()) return try { - const response = await fetch(`/api/projects/${projectId}/tasks/${editingTask.id}`, { + const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${editingTask.id}`, { method: 'PUT', - headers: { - ...getAuthHeaders(), - 'Content-Type': 'application/json' - }, body: JSON.stringify({ title: editTaskTitle, description: editTaskDescription || null, @@ -211,9 +199,8 @@ export function ProjectTasks() { if (!confirm('Are you sure you want to delete this task?')) return try { - const response = await fetch(`/api/projects/${projectId}/tasks/${taskId}`, { + const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${taskId}`, { method: 'DELETE', - headers: getAuthHeaders() }) if (response.ok) { @@ -252,12 +239,8 @@ export function ProjectTasks() { )) try { - const response = await fetch(`/api/projects/${projectId}/tasks/${taskId}`, { + const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${taskId}`, { method: 'PUT', - headers: { - ...getAuthHeaders(), - 'Content-Type': 'application/json' - }, body: JSON.stringify({ title: task.title, description: task.description, diff --git a/frontend/src/pages/users.tsx b/frontend/src/pages/users.tsx index f626a4dc..4ab77b60 100644 --- a/frontend/src/pages/users.tsx +++ b/frontend/src/pages/users.tsx @@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Alert, AlertDescription } from '@/components/ui/alert' -import { User, ApiResponse } from 'shared/types' +import { User, ApiResponse } from '@/types' import { UserForm } from '@/components/users/user-form' import { makeAuthenticatedRequest, authStorage } from '@/lib/auth' import { Plus, Edit, Trash2, Calendar, AlertCircle, Loader2, Shield, User as UserIcon } from 'lucide-react' @@ -142,7 +142,7 @@ export function Users() {
- Joined {new Date(user.created_at).toLocaleDateString()} + Joined {user.created_at ? new Date(user.created_at).toLocaleDateString() : 'Unknown'} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 00000000..8dcae3db --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,76 @@ +// Shared types for the frontend application +export interface User { + id: string + email: string + is_admin: boolean + created_at?: Date + updated_at?: Date +} + +export interface ApiResponse { + success: boolean + data: T | null + message: string | null +} + +export interface LoginRequest { + email: string + password: string +} + +export interface LoginResponse { + user: User + token: string +} + +export interface Project { + id: string + name: string + owner_id: string + created_at: Date + updated_at: Date +} + +export interface CreateProject { + name: string +} + +export interface UpdateProject { + name: string | null +} + +export interface Task { + id: string + project_id: string + title: string + description: string | null + status: TaskStatus + created_at: string + updated_at: string +} + +export type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelled" + +export interface CreateTask { + project_id: string + title: string + description: string | null +} + +export interface UpdateTask { + title: string | null + description: string | null + status: TaskStatus | null +} + +export interface CreateUser { + email: string + password: string + is_admin: boolean | null +} + +export interface UpdateUser { + email: string | null + password: string | null + is_admin: boolean | null +}