feat: implement user authentication module

- Backend (FastAPI):
  - External API authentication (pj-auth-api.vercel.app)
  - JWT token validation with Redis session storage
  - RBAC with department isolation
  - User, Role, Department models with pjctrl_ prefix
  - Alembic migrations with project-specific version table
  - Complete test coverage (13 tests)

- Frontend (React + Vite):
  - AuthContext for state management
  - Login page with error handling
  - Protected route component
  - Dashboard with user info display

- OpenSpec:
  - 7 capability specs defined
  - add-user-auth change archived

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2025-12-28 23:41:37 +08:00
commit 1fda7da2c2
77 changed files with 6562 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
import { authApi, User, LoginRequest } from '../services/api'
interface AuthContextType {
user: User | null
isAuthenticated: boolean
loading: boolean
login: (data: LoginRequest) => Promise<void>
logout: () => Promise<void>
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
// Check for existing token on mount
const token = localStorage.getItem('token')
const storedUser = localStorage.getItem('user')
if (token && storedUser) {
try {
setUser(JSON.parse(storedUser))
} catch {
localStorage.removeItem('token')
localStorage.removeItem('user')
}
}
setLoading(false)
}, [])
const login = async (data: LoginRequest) => {
const response = await authApi.login(data)
localStorage.setItem('token', response.access_token)
localStorage.setItem('user', JSON.stringify(response.user))
setUser(response.user)
}
const logout = async () => {
try {
await authApi.logout()
} catch {
// Ignore errors on logout
} finally {
localStorage.removeItem('token')
localStorage.removeItem('user')
setUser(null)
}
}
return (
<AuthContext.Provider
value={{
user,
isAuthenticated: !!user,
loading,
login,
logout,
}}
>
{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
}