Improve auth
This commit is contained in:
@@ -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 (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">Checking authentication...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
if (!isAuthenticated) {
|
||||
return <LoginForm onSuccess={handleLogin} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{showNavbar && <Navbar onLogout={handleLogout} />}
|
||||
{showNavbar && <Navbar onLogout={logout} />}
|
||||
<div className={showNavbar && location.pathname !== '/' ? "max-w-7xl mx-auto p-6 sm:p-8" : ""}>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
@@ -48,7 +52,9 @@ function AppContent() {
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<AppContent />
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<LoginResponse> = 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')
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Project> = 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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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<Project[]> = 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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
<CardDescription className="flex items-center">
|
||||
<Calendar className="mr-1 h-3 w-3" />
|
||||
Joined {new Date(user.created_at).toLocaleDateString()}
|
||||
Joined {user.created_at ? new Date(user.created_at).toLocaleDateString() : 'Unknown'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
120
frontend/src/contexts/auth-context.tsx
Normal file
120
frontend/src/contexts/auth-context.tsx
Normal file
@@ -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<void>
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isAuthenticatedState, setIsAuthenticated] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const checkAuthStatus = async (): Promise<boolean> => {
|
||||
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 (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -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 = '/'
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<Project> = 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<Task[]> = 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,
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
<CardDescription className="flex items-center">
|
||||
<Calendar className="mr-1 h-3 w-3" />
|
||||
Joined {new Date(user.created_at).toLocaleDateString()}
|
||||
Joined {user.created_at ? new Date(user.created_at).toLocaleDateString() : 'Unknown'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
76
frontend/src/types/index.ts
Normal file
76
frontend/src/types/index.ts
Normal file
@@ -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<T> {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user