feat: implement Account page and ThemeProvider; refactor App component and sidebar navigation
This commit is contained in:
128
src/App.tsx
128
src/App.tsx
@@ -1,76 +1,86 @@
|
|||||||
import { AppLayout } from '@/components/AppLayout'
|
import { AppLayout } from '@/components/AppLayout'
|
||||||
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
||||||
import { AuthProvider } from '@/contexts/AuthContext'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { AuthProvider } from '@/contexts/AuthContext'
|
||||||
|
import { AccountPage } from '@/pages/AccountPage'
|
||||||
import { ActivityPage } from '@/pages/ActivityPage'
|
import { ActivityPage } from '@/pages/ActivityPage'
|
||||||
import { AdminUsersPage } from '@/pages/AdminUsersPage'
|
import { AdminUsersPage } from '@/pages/AdminUsersPage'
|
||||||
import { DashboardPage } from '@/pages/DashboardPage'
|
import { DashboardPage } from '@/pages/DashboardPage'
|
||||||
import { LoginPage } from '@/pages/LoginPage'
|
import { LoginPage } from '@/pages/LoginPage'
|
||||||
import { RegisterPage } from '@/pages/RegisterPage'
|
import { RegisterPage } from '@/pages/RegisterPage'
|
||||||
import { SettingsPage } from '@/pages/SettingsPage'
|
|
||||||
import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router'
|
import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router'
|
||||||
|
import { ThemeProvider } from './components/ThemeProvider'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
<Router>
|
<AuthProvider>
|
||||||
<Routes>
|
<Router>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Routes>
|
||||||
<Route path="/register" element={<RegisterPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
|
||||||
{/* Protected routes with layout */}
|
{/* Protected routes with layout */}
|
||||||
<Route
|
<Route
|
||||||
path="/dashboard"
|
path="/dashboard"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<AppLayout title="Dashboard" description="Welcome to your dashboard">
|
<AppLayout
|
||||||
<DashboardPage />
|
title="Dashboard"
|
||||||
</AppLayout>
|
description="Welcome to your dashboard"
|
||||||
</ProtectedRoute>
|
>
|
||||||
}
|
<DashboardPage />
|
||||||
/>
|
</AppLayout>
|
||||||
<Route
|
</ProtectedRoute>
|
||||||
path="/activity"
|
}
|
||||||
element={
|
/>
|
||||||
<ProtectedRoute>
|
<Route
|
||||||
<AppLayout title="Activity" description="View recent activity and logs">
|
path="/activity"
|
||||||
<ActivityPage />
|
element={
|
||||||
</AppLayout>
|
<ProtectedRoute>
|
||||||
</ProtectedRoute>
|
<AppLayout
|
||||||
}
|
title="Activity"
|
||||||
/>
|
description="View recent activity and logs"
|
||||||
<Route
|
>
|
||||||
path="/settings"
|
<ActivityPage />
|
||||||
element={
|
</AppLayout>
|
||||||
<ProtectedRoute>
|
</ProtectedRoute>
|
||||||
<AppLayout title="Settings" description="Manage your account settings and preferences">
|
}
|
||||||
<SettingsPage />
|
/>
|
||||||
</AppLayout>
|
<Route
|
||||||
</ProtectedRoute>
|
path="/account"
|
||||||
}
|
element={
|
||||||
/>
|
<ProtectedRoute>
|
||||||
<Route
|
<AppLayout
|
||||||
path="/admin/users"
|
title="Account"
|
||||||
element={
|
description="Manage your account settings and preferences"
|
||||||
<ProtectedRoute requireAdmin>
|
>
|
||||||
<AppLayout
|
<AccountPage />
|
||||||
title="User Management"
|
</AppLayout>
|
||||||
description="Manage users and their permissions"
|
</ProtectedRoute>
|
||||||
headerActions={
|
}
|
||||||
<Button>Add User</Button>
|
/>
|
||||||
}
|
<Route
|
||||||
>
|
path="/admin/users"
|
||||||
<AdminUsersPage />
|
element={
|
||||||
</AppLayout>
|
<ProtectedRoute requireAdmin>
|
||||||
</ProtectedRoute>
|
<AppLayout
|
||||||
}
|
title="User Management"
|
||||||
/>
|
description="Manage users and their permissions"
|
||||||
|
headerActions={<Button>Add User</Button>}
|
||||||
|
>
|
||||||
|
<AdminUsersPage />
|
||||||
|
</AppLayout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
73
src/components/ThemeProvider.tsx
Normal file
73
src/components/ThemeProvider.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { createContext, useContext, useEffect, useState } from "react"
|
||||||
|
|
||||||
|
type Theme = "dark" | "light" | "system"
|
||||||
|
|
||||||
|
type ThemeProviderProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
defaultTheme?: Theme
|
||||||
|
storageKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeProviderState = {
|
||||||
|
theme: Theme
|
||||||
|
setTheme: (theme: Theme) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ThemeProviderState = {
|
||||||
|
theme: "system",
|
||||||
|
setTheme: () => null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = "system",
|
||||||
|
storageKey = "vite-ui-theme",
|
||||||
|
...props
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setTheme] = useState<Theme>(
|
||||||
|
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement
|
||||||
|
|
||||||
|
root.classList.remove("light", "dark")
|
||||||
|
|
||||||
|
if (theme === "system") {
|
||||||
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.matches
|
||||||
|
? "dark"
|
||||||
|
: "light"
|
||||||
|
|
||||||
|
root.classList.add(systemTheme)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root.classList.add(theme)
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
theme,
|
||||||
|
setTheme: (theme: Theme) => {
|
||||||
|
localStorage.setItem(storageKey, theme)
|
||||||
|
setTheme(theme)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProviderContext.Provider {...props} value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeProviderContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeProviderContext)
|
||||||
|
|
||||||
|
if (context === undefined)
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider")
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -7,10 +6,10 @@ import {
|
|||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
} from '@/components/ui/sidebar'
|
} from '@/components/ui/sidebar'
|
||||||
import { useSidebar } from '@/components/ui/sidebar'
|
|
||||||
import { useAuth } from '@/contexts/AuthContext'
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
import { Activity, Home, LogOut, Settings, Users } from 'lucide-react'
|
import { Activity, Home, Users } from 'lucide-react'
|
||||||
import { Link, useLocation } from 'react-router'
|
import { Link, useLocation } from 'react-router'
|
||||||
import { NavUser } from './NavUser'
|
import { NavUser } from './NavUser'
|
||||||
|
|
||||||
@@ -25,11 +24,6 @@ const navigationItems = [
|
|||||||
href: '/activity',
|
href: '/activity',
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Settings',
|
|
||||||
href: '/settings',
|
|
||||||
icon: Settings,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const adminNavigationItems = [
|
const adminNavigationItems = [
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from '../ui/sidebar'
|
} from '../ui/sidebar'
|
||||||
|
import { Link } from 'react-router'
|
||||||
|
|
||||||
interface NavUserProps {
|
interface NavUserProps {
|
||||||
user: User
|
user: User
|
||||||
@@ -65,10 +66,10 @@ export function NavUser({ user, logout }: NavUserProps) {
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<a href="/account">
|
<Link to="/account">
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
Account
|
Account
|
||||||
</a>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import { PanelLeftIcon } from "lucide-react"
|
import { PanelLeftIcon } from "lucide-react"
|
||||||
|
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
|
|||||||
595
src/pages/AccountPage.tsx
Normal file
595
src/pages/AccountPage.tsx
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
import { useTheme } from '@/components/ThemeProvider'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export function AccountPage() {
|
||||||
|
const { user, refreshUser } = useAuth()
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(true)
|
||||||
|
const [name, setName] = useState(user?.name || '')
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [apiToken, setApiToken] = useState('')
|
||||||
|
const [showFullToken, setShowFullToken] = useState(false)
|
||||||
|
const tokenInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
// Password management state
|
||||||
|
const [currentPassword, setCurrentPassword] = useState('')
|
||||||
|
const [newPassword, setNewPassword] = useState('')
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('')
|
||||||
|
const [showPasswordForm, setShowPasswordForm] = useState(false)
|
||||||
|
|
||||||
|
// Refresh user data when component mounts (run only once)
|
||||||
|
useEffect(() => {
|
||||||
|
const refreshOnMount = async () => {
|
||||||
|
try {
|
||||||
|
setIsRefreshing(true)
|
||||||
|
await refreshUser()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'Failed to refresh user data on account page load:',
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
setError('Failed to load latest user data')
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshOnMount()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []) // Intentionally empty dependency array to run only once on mount
|
||||||
|
|
||||||
|
// Update local name state when user data changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.name) {
|
||||||
|
setName(user.name)
|
||||||
|
}
|
||||||
|
}, [user?.name])
|
||||||
|
|
||||||
|
const handleUpdateProfile = async () => {
|
||||||
|
if (!name.trim()) {
|
||||||
|
setError('Name cannot be empty')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setError('')
|
||||||
|
setMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:5000/api/auth/profile', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ name: name.trim() }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setMessage('Profile updated successfully!')
|
||||||
|
await refreshUser() // Refresh user data in context
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Failed to update profile')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update profile:', error)
|
||||||
|
setError('Failed to update profile')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRegenerateApiToken = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError('')
|
||||||
|
setMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
'http://localhost:5000/api/auth/regenerate-api-token',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setApiToken(data.api_token)
|
||||||
|
setShowFullToken(true)
|
||||||
|
setMessage(
|
||||||
|
"New API token generated successfully! Copy it now - it won't be shown again.",
|
||||||
|
)
|
||||||
|
await refreshUser() // Refresh user data
|
||||||
|
|
||||||
|
// Auto-select the token text for easy copying
|
||||||
|
setTimeout(() => {
|
||||||
|
if (tokenInputRef.current) {
|
||||||
|
tokenInputRef.current.select()
|
||||||
|
tokenInputRef.current.focus()
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Failed to regenerate API token')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to regenerate API token:', error)
|
||||||
|
setError('Failed to regenerate API token')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopyToken = async () => {
|
||||||
|
if (!apiToken) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(apiToken)
|
||||||
|
setMessage('API token copied to clipboard!')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy token:', error)
|
||||||
|
setError('Failed to copy token to clipboard')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleHideToken = () => {
|
||||||
|
setApiToken('')
|
||||||
|
setShowFullToken(false)
|
||||||
|
setMessage('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleThemeChange = (newTheme: string) => {
|
||||||
|
setTheme(newTheme as 'light' | 'dark' | 'system')
|
||||||
|
setMessage('Theme updated successfully!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePasswordUpdate = async () => {
|
||||||
|
if (!newPassword.trim()) {
|
||||||
|
setError('New password is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
setError('Password must be at least 6 characters long')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setError('New password and confirmation do not match')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user logged in via password (requires current password)
|
||||||
|
// If logged in via OAuth, they can change password without current password
|
||||||
|
const loggedInViaPassword = user?.provider === 'password'
|
||||||
|
if (loggedInViaPassword && !currentPassword.trim()) {
|
||||||
|
setError('Current password is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setError('')
|
||||||
|
setMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestBody: any = { new_password: newPassword }
|
||||||
|
if (loggedInViaPassword) {
|
||||||
|
requestBody.current_password = currentPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:5000/api/auth/password', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setMessage(loggedInViaPassword ? 'Password changed successfully!' : 'Password set successfully!')
|
||||||
|
setCurrentPassword('')
|
||||||
|
setNewPassword('')
|
||||||
|
setConfirmPassword('')
|
||||||
|
setShowPasswordForm(false)
|
||||||
|
await refreshUser() // Refresh user data
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Failed to update password')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update password:', error)
|
||||||
|
setError('Failed to update password')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPasswordForm = () => {
|
||||||
|
setCurrentPassword('')
|
||||||
|
setNewPassword('')
|
||||||
|
setConfirmPassword('')
|
||||||
|
setShowPasswordForm(false)
|
||||||
|
setError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) return null
|
||||||
|
|
||||||
|
if (isRefreshing) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Loading latest account information...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Success/Error Messages */}
|
||||||
|
{message && (
|
||||||
|
<div className="p-4 rounded-md bg-green-50 border border-green-200">
|
||||||
|
<p className="text-green-800 text-sm">{message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 rounded-md bg-red-50 border border-red-200">
|
||||||
|
<p className="text-red-800 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Profile Information</CardTitle>
|
||||||
|
<CardDescription>Update your personal information</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Full Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
placeholder="Enter your full name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input id="email" value={user.email} disabled />
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Email cannot be changed directly. Contact support if needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleUpdateProfile} disabled={isLoading}>
|
||||||
|
{isLoading ? 'Saving...' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Password Management</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{user?.provider === 'password'
|
||||||
|
? 'Change your account password'
|
||||||
|
: 'Set or update your account password'
|
||||||
|
}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{!showPasswordForm ? (
|
||||||
|
<Button onClick={() => setShowPasswordForm(true)} variant="outline">
|
||||||
|
{user?.provider === 'password'
|
||||||
|
? 'Change Password'
|
||||||
|
: 'Set/Update Password'
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{user?.provider === 'password' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="current-password">Current Password</Label>
|
||||||
|
<Input
|
||||||
|
id="current-password"
|
||||||
|
type="password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
placeholder="Enter your current password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{user?.provider !== 'password' && (
|
||||||
|
<div className="p-3 bg-blue-50 border border-blue-200 rounded-md">
|
||||||
|
<p className="text-blue-800 text-sm">
|
||||||
|
💡 Since you're logged in via {user?.provider}, you can set or update your password without entering the current one.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="new-password">New Password</Label>
|
||||||
|
<Input
|
||||||
|
id="new-password"
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
placeholder="Enter your new password"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Password must be at least 6 characters long
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="confirm-password">Confirm New Password</Label>
|
||||||
|
<Input
|
||||||
|
id="confirm-password"
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="Confirm your new password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handlePasswordUpdate}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Updating...' : 'Update Password'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={resetPasswordForm}
|
||||||
|
variant="outline"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Account Information</CardTitle>
|
||||||
|
<CardDescription>View your account details</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>User ID</Label>
|
||||||
|
<Input value={user.id} disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Role</Label>
|
||||||
|
<Input value={user.role} disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Status</Label>
|
||||||
|
<Input value={user.is_active ? 'Active' : 'Disabled'} disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Authentication Methods</Label>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
{user.providers.map(provider => (
|
||||||
|
<span
|
||||||
|
key={provider}
|
||||||
|
className="px-2 py-1 bg-secondary rounded-md text-xs font-medium"
|
||||||
|
>
|
||||||
|
{provider}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Plan & Credits</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
View your current plan and credit usage
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{user.plan ? (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Current Plan</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input value={user.plan.name} disabled />
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 rounded-md text-xs font-medium ${
|
||||||
|
user.plan.code === 'pro'
|
||||||
|
? 'bg-purple-100 text-purple-800'
|
||||||
|
: user.plan.code === 'premium'
|
||||||
|
? 'bg-blue-100 text-blue-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{user.plan.code.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Plan Description</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{user.plan.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Current Credits</Label>
|
||||||
|
<Input value={user.credits ?? 0} disabled />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Plan Credit Limit</Label>
|
||||||
|
<Input value={user.plan.max_credits} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Credit Usage</Label>
|
||||||
|
<div className="w-full bg-secondary rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-primary h-2 rounded-full transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(100, ((user.credits ?? 0) / user.plan.max_credits) * 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{user.credits ?? 0} of {user.plan.max_credits} credits
|
||||||
|
remaining
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<Button variant="outline" disabled>
|
||||||
|
Upgrade Plan
|
||||||
|
</Button>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Plan upgrades coming soon
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-4">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
No plan information available
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>API Access</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Manage your API token for programmatic access
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>API Token</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
ref={tokenInputRef}
|
||||||
|
value={
|
||||||
|
showFullToken && apiToken
|
||||||
|
? apiToken
|
||||||
|
: user.api_token
|
||||||
|
? `${user.api_token.substring(0, 8)}...`
|
||||||
|
: 'No token'
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
|
className="font-mono text-sm"
|
||||||
|
placeholder="Generate a new token to see it here"
|
||||||
|
/>
|
||||||
|
{showFullToken && apiToken && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={handleCopyToken}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleHideToken}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Hide
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{showFullToken && apiToken
|
||||||
|
? "Copy this token now - it won't be displayed again once hidden."
|
||||||
|
: 'Use this token for API authentication. Keep it secure.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleRegenerateApiToken}
|
||||||
|
disabled={isLoading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Generating...' : 'Regenerate API Token'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showFullToken && apiToken && (
|
||||||
|
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||||
|
<p className="text-yellow-800 text-sm font-medium">
|
||||||
|
⚠️ Security Warning
|
||||||
|
</p>
|
||||||
|
<p className="text-yellow-700 text-xs mt-1">
|
||||||
|
Store this token securely. Anyone with this token can access
|
||||||
|
your account via API.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Preferences</CardTitle>
|
||||||
|
<CardDescription>Customize your experience</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Theme</Label>
|
||||||
|
<select
|
||||||
|
className="w-full h-9 px-3 rounded-md border border-input bg-background"
|
||||||
|
value={theme}
|
||||||
|
onChange={e => handleThemeChange(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
<option value="system">System</option>
|
||||||
|
</select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Choose your preferred theme or use system setting
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { useAuth } from '@/contexts/AuthContext'
|
|
||||||
|
|
||||||
export function SettingsPage() {
|
|
||||||
const { user } = useAuth()
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
// Simulate API call
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRegenerateApiToken = async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/auth/regenerate-api-token', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
alert(`New API token: ${data.api_token}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to regenerate API token:', error)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Profile Information</CardTitle>
|
|
||||||
<CardDescription>Update your personal information</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="name">Full Name</Label>
|
|
||||||
<Input id="name" defaultValue={user.name} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email">Email</Label>
|
|
||||||
<Input id="email" defaultValue={user.email} disabled />
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Email cannot be changed directly. Contact support if needed.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button onClick={handleSave} disabled={isLoading}>
|
|
||||||
{isLoading ? 'Saving...' : 'Save Changes'}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Account Information</CardTitle>
|
|
||||||
<CardDescription>View your account details</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>User ID</Label>
|
|
||||||
<Input value={user.id} disabled />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Role</Label>
|
|
||||||
<Input value={user.role} disabled />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Status</Label>
|
|
||||||
<Input value={user.is_active ? 'Active' : 'Disabled'} disabled />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Authentication Methods</Label>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
{user.providers.map((provider) => (
|
|
||||||
<span
|
|
||||||
key={provider}
|
|
||||||
className="px-2 py-1 bg-secondary rounded-md text-xs font-medium"
|
|
||||||
>
|
|
||||||
{provider}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>API Access</CardTitle>
|
|
||||||
<CardDescription>Manage your API token for programmatic access</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>API Token</Label>
|
|
||||||
<Input
|
|
||||||
value={user.api_token ? `${user.api_token.substring(0, 8)}...` : 'No token'}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Use this token for API authentication. Keep it secure.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button onClick={handleRegenerateApiToken} disabled={isLoading} variant="outline">
|
|
||||||
{isLoading ? 'Generating...' : 'Regenerate API Token'}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Preferences</CardTitle>
|
|
||||||
<CardDescription>Customize your experience</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Theme</Label>
|
|
||||||
<select className="w-full h-9 px-3 rounded-md border border-input bg-background">
|
|
||||||
<option>Light</option>
|
|
||||||
<option>Dark</option>
|
|
||||||
<option>System</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Language</Label>
|
|
||||||
<select className="w-full h-9 px-3 rounded-md border border-input bg-background">
|
|
||||||
<option>English</option>
|
|
||||||
<option>French</option>
|
|
||||||
<option>Spanish</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button onClick={handleSave} disabled={isLoading}>
|
|
||||||
{isLoading ? 'Saving...' : 'Save Preferences'}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
interface Plan {
|
||||||
|
id: number
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
credits: number
|
||||||
|
max_credits: number
|
||||||
|
}
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string
|
id: string
|
||||||
email: string
|
email: string
|
||||||
@@ -9,6 +18,8 @@ interface User {
|
|||||||
providers: string[]
|
providers: string[]
|
||||||
api_token?: string
|
api_token?: string
|
||||||
api_token_expires_at?: string | null
|
api_token_expires_at?: string | null
|
||||||
|
plan?: Plan
|
||||||
|
credits?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthResponse {
|
interface AuthResponse {
|
||||||
@@ -125,4 +136,4 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const authService = new AuthService()
|
export const authService = new AuthService()
|
||||||
export type { User }
|
export type { User, Plan }
|
||||||
|
|||||||
Reference in New Issue
Block a user