Commit 80763c81 authored by ZeinabRm13's avatar ZeinabRm13

Set up authentication

parent 460139e0
"""add blacklisted_tokens table
Revision ID: cb74e1cbcb81
Revises: b052cb31f4a8
Create Date: 2025-07-26 16:58:02.261980
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'cb74e1cbcb81'
down_revision: Union[str, Sequence[str], None] = 'b052cb31f4a8'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('blacklisted_tokens',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('token', sa.String(length=512), nullable=False),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('token')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('blacklisted_tokens')
# ### end Alembic commands ###
File added
import { RouterProvider } from 'react-router-dom';
import { router } from './Routes/Routes';
import { AuthProvider } from './contexts/AuthContext';
function App() {
return <RouterProvider router={router} />;
return (
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
);
}
export default App;
\ No newline at end of file
import { createBrowserRouter } from 'react-router-dom';
import { Login } from '../pages/Auth/Login';
import { Register } from '../pages/Auth/Register';
import { AskQuestion } from '../pages/Charts/AskQuestion';
import { AnalyzeChart } from '../pages/Charts/AnalyzeChart';
import { Home } from '../pages/Home';
import { ChatInterface } from '../components/ChatInteface';
// src/router.tsx
import { createBrowserRouter } from "react-router-dom"
import { Login } from "../pages/Auth/Login"
import { Register } from "../pages/Auth/Register"
import { AskQuestion } from "../pages/Charts/AskQuestion"
import { AnalyzeChart } from "../pages/Charts/AnalyzeChart"
import { Home } from "../pages/Home"
import { ChatInterface } from "../components/ChatInteface"
import { ProtectedRoute } from "../components/ProtectedRoute"
export const router = createBrowserRouter([
{
path: '/',
path: "/",
element: <Home />,
},
{
path: '/auth/login',
path: "/auth/login",
element: <Login />,
},
{
path: '/auth/register',
path: "/auth/register",
element: <Register />,
},
{
path: '/charts/ask',
path: "/charts/ask",
element: <AskQuestion />,
},
{
path: '/charts/analyze',
path: "/charts/analyze",
element: <AnalyzeChart />,
},
{
path: '/charts/chat',
element: <ChatInterface />,
path: "/charts/chat",
element: (
<ProtectedRoute>
<ChatInterface />
</ProtectedRoute>
),
},
]);
\ No newline at end of file
// Add a fallback 404 page
{
path: "*",
element: <div>Page Not Found</div>,
},
])
"use client"
// src/components/ProtectedRoute.tsx
import type { ReactNode } from "react"
import { Navigate, useLocation } from "react-router-dom"
import { useAuth } from "../contexts/AuthContext"
interface ProtectedRouteProps {
children: ReactNode
/**
* Optional redirect path (defaults to '/auth/login')
*/
redirectTo?: string
}
export const ProtectedRoute = ({ children, redirectTo = "/auth/login" }: ProtectedRouteProps) => {
const { isAuthenticated, isLoading } = useAuth()
const location = useLocation()
// Show loading while checking authentication
if (isLoading) {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
fontSize: "1.2rem",
}}
>
Loading...
</div>
)
}
if (!isAuthenticated) {
return (
<Navigate
to={redirectTo}
replace
state={{ from: location }} // Preserve navigation history
/>
)
}
return <>{children}</>
}
"use client"
// src/contexts/AuthContext.tsx
import { createContext, useContext, useState, useEffect } from "react"
import type { ReactNode } from "react"
import { authService } from "../services/api"
interface AuthContextType {
token: string | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isAuthenticated: boolean
isLoading: boolean
}
export const AuthContext = createContext<AuthContextType | null>(null)
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [token, setToken] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
// Initialize token from localStorage on mount
useEffect(() => {
const storedToken = localStorage.getItem("token")
if (storedToken) {
setToken(storedToken)
}
setIsLoading(false)
}, [])
const login = async (email: string, password: string): Promise<void> => {
try {
const response = await authService.login(email, password)
const { access_token } = response.data
localStorage.setItem("token", access_token)
setToken(access_token)
} catch (error) {
console.error("Login failed:", error)
throw error
}
}
const logout = () => {
localStorage.removeItem("token")
setToken(null)
// Redirect to home page after logout
window.location.href = "/"
}
const value = {
token,
login,
logout,
isAuthenticated: !!token,
isLoading,
}
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export const useAuth = () => {
const context = useContext(AuthContext)
if (!context) {
throw new Error("useAuth must be used within an AuthProvider")
}
return context
}
......@@ -3,36 +3,56 @@
import type React from "react"
import { useState } from "react"
import { authService } from "../../services/api"
import { useNavigate, Link } from "react-router-dom"
import { useNavigate, Link, useLocation } from "react-router-dom"
import { useAuth } from "../../contexts/AuthContext"
import "../../styles/auth.css"
export const Login = () => {
const [formData, setFormData] = useState({
username: "",
email: "",
password: "",
})
const [error, setError] = useState("")
const [isLoading, setIsLoading] = useState(false)
const navigate = useNavigate()
const location = useLocation()
const { login } = useAuth()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!formData.email || !formData.password) {
setError("Please fill in all fields")
return
}
setIsLoading(true)
setError("")
try {
setError("")
const response = await authService.login(formData)
localStorage.setItem("token", response.data.token)
localStorage.setItem("username", formData.username)
navigate("/")
await login(formData.email, formData.password)
// Redirect to previous page or chat if no previous page
const from = location.state?.from?.pathname || "/charts/chat"
navigate(from, { replace: true })
} catch (err: any) {
setError(err.response?.data?.detail || "Invalid credentials")
console.error("Login error:", err)
setError(err.response?.data?.detail || "Login failed. Please check your credentials and try again.")
} finally {
setIsLoading(false)
}
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { id, value } = e.target
setFormData((prev) => ({
...prev,
[id]: value,
}))
// Clear error when user starts typing
if (error) setError("")
}
return (
<div className="auth-container">
<div className="auth-card">
......@@ -42,18 +62,25 @@ export const Login = () => {
</div>
<div className="auth-content">
{error && <div className="error-message">{error}</div>}
{error && (
<div className="error-message">
<span className="error-icon">⚠️</span>
{error}
</div>
)}
<form onSubmit={handleSubmit} className="auth-form">
<div className="form-group">
<label htmlFor="username">Username</label>
<label htmlFor="email">Email Address</label>
<input
id="username"
type="text"
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
id="email"
type="email"
value={formData.email}
onChange={handleInputChange}
required
placeholder="Enter your username"
autoComplete="username"
placeholder="Enter your email"
className={error ? "error-input" : ""}
/>
</div>
......@@ -63,21 +90,47 @@ export const Login = () => {
id="password"
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
onChange={handleInputChange}
required
autoComplete="current-password"
placeholder="Enter your password"
className={error ? "error-input" : ""}
/>
<Link to="/auth/forgot-password" className="forgot-password">
Forgot password?
</Link>
</div>
<button type="submit" className="submit-button" disabled={isLoading}>
{isLoading ? "Signing in..." : "Sign In"}
<button type="submit" className={`submit-button ${isLoading ? "loading" : ""}`} disabled={isLoading}>
{isLoading ? (
<>
<span className="spinner"></span>
Signing In...
</>
) : (
"Sign In"
)}
</button>
</form>
<div className="auth-divider">
<span>OR</span>
</div>
<div className="social-login">
<button type="button" className="social-button google" disabled={isLoading}>
Continue with Google
</button>
<button type="button" className="social-button github" disabled={isLoading}>
Continue with GitHub
</button>
</div>
</div>
<div className="auth-footer">
<p>Don't have an account?</p>
<Link to="/auth/register">Create one here</Link>
<p>
Don't have an account? <Link to="/auth/register">Sign up</Link>
</p>
</div>
</div>
</div>
......
......@@ -37,7 +37,7 @@ export const Register = () => {
})
setSuccess(true)
// Auto-redirect after 2 seconds
setTimeout(() => navigate("/auth/login"), 2000)
setTimeout(() => navigate("/charts/chat"), 2000)
} catch (err: any) {
setError(err.response?.data?.detail || "Registration failed")
}
......@@ -48,7 +48,7 @@ export const Register = () => {
<div className="success-message">
<div className="success-icon"></div>
<h2>Registration Successful!</h2>
<p>You will be redirected to login page shortly...</p>
<p>You will be redirected to start your chat.....</p>
</div>
)
}
......
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { appService } from '../services/api';
// import { authService } from '../services/api';
import '../styles/home.css';
"use client"
import { useState, useEffect } from "react"
import { Link } from "react-router-dom"
import { appService } from "../services/api"
import { useAuth } from "../contexts/AuthContext"
import "../styles/home.css"
export const Home = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [serverStatus, setServerStatus] = useState<'loading' | 'online' | 'offline'>('loading');
const [userData, setUserData] = useState<{ username: string } | null>(null);
const { isAuthenticated, logout, isLoading } = useAuth()
const [serverStatus, setServerStatus] = useState<"loading" | "online" | "offline">("loading")
const [featureCards] = useState([
{
title: "Chart Analysis",
description: "Upload and analyze your charts with our powerful tools",
path: "/charts/analyze",
public: true,
},
{
title: "Ask Questions",
description: "Get insights about your data by asking natural language questions",
path: "/charts/ask",
path: "/charts/ask",
public: true,
},
{
title: "Interactive Chat",
description: "Have a conversation with AI about your charts (Login required)",
path: "/charts/chat",
public: false,
},
]);
])
useEffect(() => {
// Check if user is authenticated
const token = localStorage.getItem('token');
setIsAuthenticated(!!token);
// Fetch server status
const checkServerStatus = async () => {
try {
await appService.getRoot();
setServerStatus('online');
await appService.getRoot()
setServerStatus("online")
} catch (error) {
setServerStatus('offline');
setServerStatus("offline")
}
};
checkServerStatus();
}
// If authenticated, fetch user data
if (token) {
const fetchUserData = async () => {
try {
// Assuming you have an endpoint to get current user
// const response = await authService.getCurrentUser();
// setUserData(response.data);
// For now, just get username from token or local storage
setUserData({ username: localStorage.getItem('username') || 'User' });
} catch (error) {
console.error('Failed to fetch user data:', error);
}
};
checkServerStatus()
}, [])
fetchUserData();
}
}, []);
const handleLogout = () => {
logout()
}
const handleLogout = async () => {
try {
// Optional: Call logout endpoint if you have one
// await authService.logout();
localStorage.removeItem('token');
localStorage.removeItem('username');
setIsAuthenticated(false);
setUserData(null);
} catch (error) {
console.error('Logout failed:', error);
}
};
if (isLoading) {
return (
<div className="home-container">
<div className="loading-spinner">Loading...</div>
</div>
)
}
return (
<div className="home-container">
<header className="home-header">
<div className="server-status">
Server Status:
<span className={`status-indicator ${serverStatus}`}>
{serverStatus.toUpperCase()}
</span>
Server Status:
<span className={`status-indicator ${serverStatus}`}>{serverStatus.toUpperCase()}</span>
</div>
<nav className="auth-nav">
{isAuthenticated ? (
<div className="user-section">
<span>Welcome, {userData?.username}</span>
<span className="welcome-text">Welcome back!</span>
<button onClick={handleLogout} className="logout-button">
Logout
</button>
</div>
) : (
<>
<Link to="/auth/login" className="auth-link">
<div className="auth-links">
<Link to="/auth/login" className="auth-link login">
Login
</Link>
<Link to="/auth/register" className="auth-link">
<Link to="/auth/register" className="auth-link register">
Register
</Link>
</>
</div>
)}
</nav>
</header>
......@@ -104,8 +88,8 @@ export const Home = () => {
<section className="hero-section">
<h1>Data Visualization & Analysis Platform</h1>
<p>
Transform your data into insights with our powerful chart analysis tools
and AI-powered question answering system.
Transform your data into insights with our powerful chart analysis tools and AI-powered question answering
system.
</p>
</section>
......@@ -116,9 +100,15 @@ export const Home = () => {
<div key={index} className="feature-card">
<h3>{feature.title}</h3>
<p>{feature.description}</p>
<Link to={feature.path} className="feature-link">
Try it out →
</Link>
{feature.public || isAuthenticated ? (
<Link to={feature.path} className="feature-link">
Try it out →
</Link>
) : (
<Link to="/auth/login" className="feature-link login-required">
Login to access →
</Link>
)}
</div>
))}
</div>
......@@ -127,6 +117,7 @@ export const Home = () => {
{!isAuthenticated && (
<section className="cta-section">
<h2>Ready to get started?</h2>
<p>Create an account to access all features including interactive chat!</p>
<div className="cta-buttons">
<Link to="/auth/register" className="cta-button primary">
Create Account
......@@ -143,5 +134,5 @@ export const Home = () => {
<p>© {new Date().getFullYear()} Data Analysis Platform. All rights reserved.</p>
</footer>
</div>
);
};
\ No newline at end of file
)
}
......@@ -10,7 +10,27 @@ const api = axios.create({
withCredentials: true, // If you need to send cookies
});
// Add request interceptor to include token
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Add response interceptor to handle 401 errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token expired or invalid - force logout
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
......@@ -18,8 +38,17 @@ export default api;
export const authService = {
register: (data: { username: string; password: string; email: string }) =>
api.post('/auth/register', data),
login: (data: { username: string; password: string }) =>
api.post('/auth/login', data),
login: (email: string, password: string) => {
return api.post('/auth/login', {
email, // Changed from username
password
}, {
headers: {
'Content-Type': 'application/json',
}
});
},
};
// Chart services
......
.auth-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1rem;
}
.auth-card {
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
width: 100%;
max-width: 400px;
margin: 2rem auto;
}
.auth-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
text-align: center;
}
.auth-header h2 {
margin: 0 0 0.5rem 0;
font-size: 1.8rem;
font-weight: 600;
}
.auth-header p {
margin: 0;
opacity: 0.9;
font-size: 0.95rem;
}
.auth-content {
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.auth-form {
display: flex;
flex-direction: column;
gap: 1rem;
gap: 1.5rem;
}
.form-group {
......@@ -20,54 +53,230 @@
.form-group label {
font-weight: 500;
color: #333;
font-size: 0.9rem;
}
.form-group input {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
padding: 0.75rem;
border: 2px solid #e1e5e9;
border-radius: 6px;
font-size: 1rem;
transition: all 0.2s ease;
background-color: #f8f9fa;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
background-color: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-group input.error-input {
border-color: #dc3545;
background-color: #fff5f5;
}
.forgot-password {
font-size: 0.85rem;
color: #667eea;
text-decoration: none;
align-self: flex-end;
margin-top: 0.25rem;
}
.forgot-password:hover {
text-decoration: underline;
}
.submit-button {
padding: 0.75rem;
background-color: #4CAF50;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 4px;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
margin-top: 1rem;
font-weight: 500;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 0.5rem;
}
.submit-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.submit-button:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
.submit-button:hover {
background-color: #45a049;
.submit-button.loading {
background: #6c757d;
}
.spinner {
width: 1rem;
height: 1rem;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.error-message {
color: #f44336;
padding: 0.5rem;
background-color: #ffebee;
border-radius: 4px;
color: #dc3545;
padding: 0.75rem;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 6px;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.error-icon {
font-size: 1.1rem;
}
.success-message {
color: #4CAF50;
text-align: center;
margin-top: 2rem;
padding: 3rem 2rem;
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
max-width: 400px;
margin: 2rem auto;
}
.success-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.success-message h2 {
color: #28a745;
margin-bottom: 1rem;
}
.success-message p {
color: #666;
}
.auth-divider {
display: flex;
align-items: center;
margin: 1.5rem 0;
color: #6c757d;
font-size: 0.85rem;
}
.auth-divider::before,
.auth-divider::after {
content: "";
flex: 1;
height: 1px;
background: #e1e5e9;
}
.auth-divider span {
padding: 0 1rem;
}
.social-login {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.social-button {
padding: 0.75rem;
border: 2px solid #e1e5e9;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.social-button:hover:not(:disabled) {
border-color: #667eea;
background-color: #f8f9ff;
}
.social-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.social-button.google {
color: #db4437;
}
.social-button.github {
color: #333;
}
.auth-footer {
margin-top: 1rem;
padding: 1.5rem 2rem;
text-align: center;
background-color: #f8f9fa;
border-top: 1px solid #e1e5e9;
}
.auth-footer p {
margin: 0;
color: #6c757d;
font-size: 0.9rem;
}
.auth-footer a {
color: #4CAF50;
color: #667eea;
text-decoration: none;
font-weight: 500;
}
.auth-footer a:hover {
text-decoration: underline;
}
\ No newline at end of file
}
/* Mobile Responsive */
@media (max-width: 480px) {
.auth-container {
padding: 0.5rem;
}
.auth-card {
border-radius: 8px;
}
.auth-header,
.auth-content,
.auth-footer {
padding: 1.5rem;
}
.auth-header h2 {
font-size: 1.5rem;
}
}
/* General Styles */
.home-container {
display: flex;
flex-direction: column;
min-height: 100vh;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
color: #333;
line-height: 1.6;
background-color: #f9f9f9;
}
/* Loading Spinner */
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-size: 1.2rem;
color: #666;
}
/* Header Styles */
.home-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: #f8f9fa;
border-bottom: 1px solid #e9ecef;
background-color: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.server-status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
font-weight: 500;
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-left: 0.5rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-weight: bold;
text-transform: uppercase;
font-size: 0.8rem;
}
.status-indicator.loading {
background-color: #ffc107;
animation: pulse 1.5s infinite;
color: #333;
}
.status-indicator.online {
background-color: #28a745;
color: white;
}
.status-indicator.offline {
background-color: #dc3545;
color: white;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
.auth-nav {
display: flex;
align-items: center;
}
.auth-nav {
.auth-links {
display: flex;
gap: 1rem;
}
.auth-link {
color: #495057;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.2s;
text-decoration: none;
font-weight: 500;
transition: all 0.2s ease;
}
.auth-link.login {
color: #007bff;
border: 1px solid #007bff;
}
.auth-link.login:hover {
background-color: #007bff;
color: white;
}
.auth-link.register {
background-color: #007bff;
color: white;
border: 1px solid #007bff;
}
.auth-link:hover {
background-color: #e9ecef;
.auth-link.register:hover {
background-color: #0056b3;
border-color: #0056b3;
}
.user-section {
......@@ -70,19 +104,28 @@
gap: 1rem;
}
.welcome-text {
color: #333;
font-weight: 500;
}
.logout-button {
background: none;
border: none;
color: #dc3545;
cursor: pointer;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
background-color: #dc3545;
color: white;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.logout-button:hover {
background-color: #f8d7da;
background-color: #c82333;
transform: translateY(-1px);
}
/* Main Content Styles */
.home-main {
flex: 1;
padding: 2rem;
......@@ -93,31 +136,33 @@
.hero-section {
text-align: center;
padding: 3rem 0;
padding: 3rem 1rem;
margin-bottom: 2rem;
}
.hero-section h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
color: #343a40;
color: #222;
}
.hero-section p {
font-size: 1.2rem;
color: #6c757d;
color: #555;
max-width: 700px;
margin: 0 auto;
}
/* Features Section */
.features-section {
margin: 3rem 0;
margin: 4rem 0;
}
.features-section h2 {
text-align: center;
font-size: 2rem;
margin-bottom: 2rem;
color: #343a40;
color: #222;
}
.feature-cards {
......@@ -129,55 +174,76 @@
.feature-card {
background-color: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
padding: 2rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.feature-card h3 {
color: #343a40;
margin-bottom: 0.5rem;
font-size: 1.5rem;
margin-bottom: 1rem;
color: #007bff;
}
.feature-card p {
color: #6c757d;
margin-bottom: 1rem;
margin-bottom: 1.5rem;
color: #555;
}
.feature-link {
color: #007bff;
display: inline-block;
padding: 0.5rem 1rem;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
font-weight: 500;
display: inline-block;
margin-top: 0.5rem;
transition: background-color 0.2s ease;
}
.feature-link:hover {
text-decoration: underline;
background-color: #0056b3;
}
.feature-link.login-required {
background-color: #ffc107;
color: #333;
}
.feature-link.login-required:hover {
background-color: #e0a800;
}
/* CTA Section */
.cta-section {
text-align: center;
padding: 3rem 0;
padding: 3rem 1rem;
margin-top: 3rem;
background-color: #f8f9fa;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.cta-section h2 {
font-size: 2rem;
margin-bottom: 1rem;
}
.cta-section p {
color: #666;
margin-bottom: 1.5rem;
color: #343a40;
}
.cta-buttons {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
}
.cta-button {
......@@ -185,7 +251,7 @@
border-radius: 4px;
text-decoration: none;
font-weight: 500;
transition: background-color 0.2s;
transition: all 0.2s ease;
}
.cta-button.primary {
......@@ -194,23 +260,71 @@
}
.cta-button.primary:hover {
background-color: #0069d9;
background-color: #0056b3;
}
.cta-button.secondary {
background-color: #6c757d;
color: white;
background-color: white;
color: #007bff;
border: 1px solid #007bff;
}
.cta-button.secondary:hover {
background-color: #5a6268;
background-color: #f0f7ff;
}
/* Footer Styles */
.home-footer {
text-align: center;
padding: 1rem;
background-color: #f8f9fa;
border-top: 1px solid #e9ecef;
color: #6c757d;
font-size: 0.9rem;
}
\ No newline at end of file
padding: 1.5rem;
background-color: #fff;
border-top: 1px solid #eee;
color: #666;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.home-header {
flex-direction: column;
gap: 1rem;
padding: 1rem;
}
.hero-section h1 {
font-size: 2rem;
}
.hero-section p {
font-size: 1rem;
}
.cta-buttons {
flex-direction: column;
align-items: center;
}
.cta-button {
width: 100%;
max-width: 250px;
}
.auth-links {
flex-direction: column;
gap: 0.5rem;
}
.user-section {
flex-direction: column;
gap: 0.5rem;
}
}
@media (max-width: 480px) {
.feature-cards {
grid-template-columns: 1fr;
}
.home-main {
padding: 1rem;
}
}
......@@ -2,7 +2,8 @@
from datetime import datetime, timezone, timedelta
from jose import jwt
from passlib.context import CryptContext
from src.domain.ports.repositories.user_repository import UserRepositoryPort
from src.domain.ports.repositories.user_repository import UserRepositoryPort
from src.domain.ports.repositories.token_repository import TokenRepositoryPort
from src.domain.entities.user import User
from src.config import Settings
import uuid
......@@ -13,21 +14,49 @@ from src.application.dtos.authentication import (
UserResponseDTO,
TokenResponseDTO
)
settings = Settings()
# Update src/application/services/auth.py
from jose import JWTError, jwt
from datetime import datetime, timezone, timedelta
class AuthService(AuthServicePort):
def __init__(
self,
user_repo: UserRepositoryPort,
secret_key: str = settings.JWT_SECRET,
token_repo: TokenRepositoryPort, # Add this
secret_key: str = Settings().JWT_SECRET,
algorithm: str = "HS256",
expires_minutes: int = 30
):
self._user_repo = user_repo
self._token_repo = token_repo
self._secret_key = secret_key
self._algorithm = algorithm
self._expires_minutes = expires_minutes
self._pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
async def logout(self, token: str) -> None:
"""Invalidate a JWT token"""
try:
payload = jwt.decode(token, self._secret_key, algorithms=[self._algorithm])
exp = payload.get("exp")
if exp:
expires_at = datetime.fromtimestamp(exp, tz=timezone.utc)
await self._token_repo.add_to_blacklist(token, expires_at)
except JWTError:
pass # Token is invalid anyway
async def validate_token(self, token: str) -> bool:
"""Check if token is valid and not blacklisted"""
try:
if await self._token_repo.is_blacklisted(token):
return False
jwt.decode(token, self._secret_key, algorithms=[self._algorithm])
return True
except JWTError:
return False
async def register(self, email, password) -> User:
if await self._user_repo.get_by_email(email):
raise ValueError("Email already registered")
......@@ -39,14 +68,18 @@ class AuthService(AuthServicePort):
)
await self._user_repo.create_user(user)
return user
async def login(self, email, password) -> str:
async def login(self, email: str, password: str) -> str:
user = await self._user_repo.get_by_email(email)
if not user or not self._verify_password(password, user.password_hash):
raise ValueError("Invalid credentials")
# Return just the token string, not the whole response
return self._create_access_token(user.email)
def _hash_password(self, password: str) -> str:
return self._pwd_context.hash(password)
......
......@@ -5,12 +5,14 @@ from sqlalchemy.orm import sessionmaker
from src.application.services.authentication_service import AuthService
from src.application.use_cases.upload_chart import UploadChartUseCase
from src.domain.ports.repositories.user_repository import UserRepositoryPort
from src.domain.ports.repositories.token_repository import TokenRepositoryPort
from src.domain.ports.repositories.charts_repository import ChartsRepositoryPort
from src.infrastructure.adapters.sqlserver.sql_user_repository import SqlUserRepository
from src.infrastructure.adapters.sqlserver.sql_charts_repository import SqlChartsRepository
from src.application.services.analyze_service import AnalyzeService
from src.application.services.llm_service import LLMService
from src.application.ports.llm_service_port import LLMServicePort
from src.infrastructure.adapters.sqlserver.sql_token_repository import SqlTokenRepository
# from infrastructure.services.llm.openai_service import OpenAIService # Concrete LLM impl
# from infrastructure.services.image.pillow_service import PillowImageService # Concrete impl
from src.config import settings
......@@ -20,9 +22,11 @@ from src.application.services.ollama_service import OllamaService
engine = create_async_engine(settings.DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
yield session
......@@ -30,21 +34,25 @@ def get_llm_service() -> LLMServicePort:
return OllamaService(host="http://172.25.1.141:11434")
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
yield session
def get_user_repository(session: AsyncSession = Depends(get_db_session)) -> UserRepositoryPort:
return SqlUserRepository(session)
def get_charts_repository(session: AsyncSession = Depends(get_db_session)) -> ChartsRepositoryPort:
return SqlChartsRepository(session)
def get_token_repo(session: AsyncSession = Depends(get_db_session)) -> SqlTokenRepository:
return SqlTokenRepository(session)
def get_auth_service(
user_repo: UserRepositoryPort = Depends(get_user_repository),
token_repo: TokenRepositoryPort = Depends(get_token_repo)
) -> AuthService:
return AuthService(
user_repo=user_repo,
secret_key=settings.JWT_SECRET,
algorithm=settings.JWT_ALGORITHM,
expires_minutes=settings.JWT_EXPIRE_MINUTES,
)
return AuthService(user_repo, token_repo)
def get_upload_use_case(charts_repo: ChartsRepositoryPort = Depends(get_charts_repository)) -> UploadChartUseCase:
return UploadChartUseCase(charts_repo)
......
# src/domain/ports/repositories/token_repository.py
from datetime import datetime
from abc import ABC, abstractmethod
class TokenRepositoryPort(ABC):
@abstractmethod
async def add_to_blacklist(self, token: str, expires_at: datetime) -> None:
pass
@abstractmethod
async def is_blacklisted(self, token: str) -> bool:
pass
\ No newline at end of file
......@@ -52,4 +52,16 @@ class ChartAnalysis(Base):
chart_image = relationship("ChartImage", back_populates="analyses")
def __repr__(self):
return f"<ChartAnalysis(id={self.id}, chart_image_id={self.chart_image_id})>"
\ No newline at end of file
return f"<ChartAnalysis(id={self.id}, chart_image_id={self.chart_image_id})>"
class BlacklistedToken(Base):
__tablename__ = 'blacklisted_tokens'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
token = Column(String(512), unique=True, nullable=False)
expires_at = Column(DateTime(timezone=True), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
def __repr__(self):
return f"<BlacklistedToken(token={self.token[:10]}...)>"
\ No newline at end of file
# src/infrastructure/adapters/sqlserver/sql_token_repository.py
from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession
from src.domain.ports.repositories.token_repository import TokenRepositoryPort
from src.infrastructure.adapters.sqlserver.models import BlacklistedToken
from datetime import datetime
class SqlTokenRepository(TokenRepositoryPort):
def __init__(self, session: AsyncSession):
self._session = session
async def add_to_blacklist(self, token: str, expires_at: datetime) -> None:
blacklisted_token = BlacklistedToken(
token=token,
expires_at=expires_at
)
self._session.add(blacklisted_token)
await self._session.commit()
async def is_blacklisted(self, token: str) -> bool:
result = await self._session.execute(
select(BlacklistedToken).where(BlacklistedToken.token == token)
)
return result.scalar_one_or_none() is not None
async def cleanup_expired(self) -> None:
"""Remove expired tokens from blacklist"""
await self._session.execute(
delete(BlacklistedToken).where(BlacklistedToken.expires_at < datetime.now())
)
await self._session.commit()
\ No newline at end of file
......@@ -2,9 +2,24 @@ from fastapi import APIRouter, Depends, HTTPException, status
from src.application.dtos.authentication import RegisterRequestDTO, LoginRequestDTO, TokenResponseDTO
from src.application.ports.authentication_service_port import AuthServicePort
from src.dependencies import get_auth_service
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
router = APIRouter(tags=["auth"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
@router.post("/logout")
async def logout(
token: str = Depends(oauth2_scheme),
auth_service: AuthServicePort = Depends(get_auth_service)
):
await auth_service.logout(token)
return {"message": "Successfully logged out"}
@router.post("/register", status_code=status.HTTP_201_CREATED)
async def register(
request: RegisterRequestDTO,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment