diff --git a/src/App.tsx b/src/App.tsx index 18daf2e..7ad94c0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,8 +3,36 @@ import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; -import Index from "./pages/Index"; -import NotFound from "./pages/NotFound"; + +// Layouts +import PublicLayout from "@/components/layouts/PublicLayout"; +import AuthenticatedLayout from "@/components/layouts/AuthenticatedLayout"; + +// Public pages +import Index from "@/pages/Index"; +import LoginPage from "@/pages/auth/LoginPage"; +import RegisterPage from "@/pages/auth/RegisterPage"; +import VerifyEmailPage from "@/pages/auth/VerifyEmailPage"; +import ForgotPasswordPage from "@/pages/auth/ForgotPasswordPage"; +import ResetPasswordPage from "@/pages/auth/ResetPasswordPage"; +import InviteAcceptPage from "@/pages/auth/InviteAcceptPage"; +import OIDCConsentPage from "@/pages/auth/OIDCConsentPage"; +import OIDCErrorPage from "@/pages/auth/OIDCErrorPage"; + +// User pages +import ProfilePage from "@/pages/user/ProfilePage"; +import SecurityPage from "@/pages/user/SecurityPage"; +import LinkedAccountsPage from "@/pages/user/LinkedAccountsPage"; +import ActivityPage from "@/pages/user/ActivityPage"; + +// Organization pages +import OrgOverviewPage from "@/pages/org/OrgOverviewPage"; +import MembersPage from "@/pages/org/MembersPage"; +import PoliciesPage from "@/pages/org/PoliciesPage"; +import OrgAuditPage from "@/pages/org/OrgAuditPage"; +import OIDCClientsPage from "@/pages/org/OIDCClientsPage"; + +import NotFound from "@/pages/NotFound"; const queryClient = new QueryClient(); @@ -15,8 +43,38 @@ const App = () => ( + {/* Index redirect */} } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + + {/* Public routes */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Authenticated routes */} + }> + {/* User routes */} + } /> + } /> + } /> + } /> + + {/* Organization routes */} + } /> + } /> + } /> + } /> + } /> + + + {/* Catch-all */} } /> diff --git a/src/components/layouts/AuthenticatedLayout.tsx b/src/components/layouts/AuthenticatedLayout.tsx new file mode 100644 index 0000000..a107200 --- /dev/null +++ b/src/components/layouts/AuthenticatedLayout.tsx @@ -0,0 +1,20 @@ +import { Outlet } from "react-router-dom"; +import { SidebarProvider } from "@/components/ui/sidebar"; +import { AppSidebar } from "@/components/navigation/AppSidebar"; +import { TopBar } from "@/components/navigation/TopBar"; + +export default function AuthenticatedLayout() { + return ( + +
+ +
+ +
+ +
+
+
+
+ ); +} diff --git a/src/components/layouts/PublicLayout.tsx b/src/components/layouts/PublicLayout.tsx new file mode 100644 index 0000000..d61b3f6 --- /dev/null +++ b/src/components/layouts/PublicLayout.tsx @@ -0,0 +1,39 @@ +import { Shield } from "lucide-react"; +import { Outlet, Link } from "react-router-dom"; + +export default function PublicLayout() { + return ( +
+ {/* Subtle gradient background */} +
+ + {/* Header */} +
+
+ +
+ +
+ Authy2 + +
+
+ + {/* Main content */} +
+
+ +
+
+ + {/* Footer */} +
+
+

+ © {new Date().getFullYear()} Authy2. Secure identity management. +

+
+
+
+ ); +} diff --git a/src/components/navigation/AppSidebar.tsx b/src/components/navigation/AppSidebar.tsx new file mode 100644 index 0000000..240290d --- /dev/null +++ b/src/components/navigation/AppSidebar.tsx @@ -0,0 +1,176 @@ +import { useLocation } from "react-router-dom"; +import { + User, + Shield, + Link2, + Activity, + Building2, + Users, + Settings, + FileText, + Key, +} from "lucide-react"; +import { NavLink } from "@/components/NavLink"; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarHeader, + SidebarFooter, + useSidebar, +} from "@/components/ui/sidebar"; +import { cn } from "@/lib/utils"; + +const userNavItems = [ + { title: "Profile", url: "/profile", icon: User }, + { title: "Security", url: "/security", icon: Shield }, + { title: "Linked Accounts", url: "/linked-accounts", icon: Link2 }, + { title: "Activity", url: "/activity", icon: Activity }, +]; + +const orgNavItems = [ + { title: "Overview", url: "/org", icon: Building2 }, + { title: "Members", url: "/org/members", icon: Users }, + { title: "Policies", url: "/org/policies", icon: Settings }, + { title: "Audit Log", url: "/org/audit", icon: FileText }, +]; + +const adminNavItems = [ + { title: "OIDC Clients", url: "/org/clients", icon: Key }, +]; + +export function AppSidebar() { + const { state } = useSidebar(); + const collapsed = state === "collapsed"; + const location = useLocation(); + + const isActive = (path: string) => location.pathname === path; + const isOrgActive = orgNavItems.some((item) => isActive(item.url)) || adminNavItems.some((item) => isActive(item.url)); + const isUserActive = userNavItems.some((item) => isActive(item.url)); + + return ( + + {/* Logo */} + +
+
+ +
+ {!collapsed && ( + + Authy2 + + )} +
+
+ + + {/* User Section */} + + + {!collapsed && "Account"} + + + + {userNavItems.map((item) => ( + + + + + {!collapsed && {item.title}} + + + + ))} + + + + + {/* Organization Section */} + + + {!collapsed && "Organization"} + + + + {orgNavItems.map((item) => ( + + + + + {!collapsed && {item.title}} + + + + ))} + + + + + {/* Admin Section */} + + + {!collapsed && "Admin"} + + + + {adminNavItems.map((item) => ( + + + + + {!collapsed && {item.title}} + + + + ))} + + + + + + + {!collapsed && ( +
+ v1.0.0 • Self-hosted +
+ )} +
+
+ ); +} diff --git a/src/components/navigation/TopBar.tsx b/src/components/navigation/TopBar.tsx new file mode 100644 index 0000000..d5f7aac --- /dev/null +++ b/src/components/navigation/TopBar.tsx @@ -0,0 +1,127 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Menu, ChevronDown, LogOut, User, Shield, Building2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { SidebarTrigger } from "@/components/ui/sidebar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; + +// Mock user data - will be replaced with real auth context +const mockUser = { + name: "John Doe", + email: "john@example.com", + avatar: null, + initials: "JD", +}; + +// Mock organization data +const mockOrgs = [ + { id: "1", name: "Acme Corp", role: "admin" }, + { id: "2", name: "Beta Inc", role: "member" }, +]; + +export function TopBar() { + const navigate = useNavigate(); + const [currentOrg, setCurrentOrg] = useState(mockOrgs[0]); + + const handleLogout = () => { + // Will be replaced with actual logout logic + navigate("/login"); + }; + + return ( +
+ {/* Left side - Sidebar toggle */} +
+ + + +
+ + {/* Right side - Org selector + User menu */} +
+ {/* Organization Selector */} + + + + + + + Switch Organization + + + {mockOrgs.map((org) => ( + setCurrentOrg(org)} + className="flex items-center justify-between" + > +
+ + {org.name} +
+ {org.role === "admin" && ( + + Admin + + )} +
+ ))} +
+
+ + {/* User Menu */} + + + + + + +
+ {mockUser.name} + + {mockUser.email} + +
+
+ + navigate("/profile")}> + + Profile + + navigate("/security")}> + + Security + + + + + Log out + +
+
+
+
+ ); +} diff --git a/src/index.css b/src/index.css index 4844bbd..42da904 100644 --- a/src/index.css +++ b/src/index.css @@ -2,95 +2,129 @@ @tailwind components; @tailwind utilities; -/* Definition of the design system. All colors, gradients, fonts, etc should be defined here. -All colors MUST be HSL. +/* Authy2 Design System - Enterprise Authentication Platform + Colors are HSL for theming flexibility */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + @layer base { :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; + /* Core palette - Deep slate with teal accent */ + --background: 210 20% 98%; + --foreground: 222 47% 11%; --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card-foreground: 222 47% 11%; --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; + --popover-foreground: 222 47% 11%; - --primary: 222.2 47.4% 11.2%; + /* Primary - Deep navy for trust */ + --primary: 222 47% 20%; --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + /* Secondary - Soft slate */ + --secondary: 215 20% 95%; + --secondary-foreground: 222 47% 20%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + /* Muted - Subtle backgrounds */ + --muted: 215 20% 96%; + --muted-foreground: 215 16% 47%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + /* Accent - Teal for actions and highlights */ + --accent: 173 58% 39%; + --accent-foreground: 0 0% 100%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; + /* Semantic colors */ + --destructive: 0 72% 51%; + --destructive-foreground: 0 0% 100%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --success: 152 69% 31%; + --success-foreground: 0 0% 100%; + + --warning: 38 92% 50%; + --warning-foreground: 0 0% 100%; + + --info: 199 89% 48%; + --info-foreground: 0 0% 100%; + + /* UI elements */ + --border: 214 32% 91%; + --input: 214 32% 91%; + --ring: 173 58% 39%; --radius: 0.5rem; - --sidebar-background: 0 0% 98%; + /* Sidebar - Darker for visual hierarchy */ + --sidebar-background: 222 47% 11%; + --sidebar-foreground: 215 20% 85%; + --sidebar-primary: 173 58% 45%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 222 40% 18%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 222 40% 20%; + --sidebar-ring: 173 58% 45%; + --sidebar-muted: 215 16% 55%; - --sidebar-foreground: 240 5.3% 26.1%; - - --sidebar-primary: 240 5.9% 10%; - - --sidebar-primary-foreground: 0 0% 98%; - - --sidebar-accent: 240 4.8% 95.9%; - - --sidebar-accent-foreground: 240 5.9% 10%; - - --sidebar-border: 220 13% 91%; - - --sidebar-ring: 217.2 91.2% 59.8%; + /* Custom gradients and effects */ + --gradient-brand: linear-gradient(135deg, hsl(222 47% 20%), hsl(222 47% 11%)); + --gradient-accent: linear-gradient(135deg, hsl(173 58% 39%), hsl(173 58% 32%)); + --gradient-subtle: linear-gradient(135deg, hsl(210 20% 98%), hsl(215 20% 96%)); + + --shadow-sm: 0 1px 2px 0 hsl(222 47% 11% / 0.05); + --shadow-md: 0 4px 6px -1px hsl(222 47% 11% / 0.1), 0 2px 4px -2px hsl(222 47% 11% / 0.1); + --shadow-lg: 0 10px 15px -3px hsl(222 47% 11% / 0.1), 0 4px 6px -4px hsl(222 47% 11% / 0.1); + --shadow-card: 0 1px 3px 0 hsl(222 47% 11% / 0.06), 0 1px 2px -1px hsl(222 47% 11% / 0.06); } .dark { - --background: 222.2 84% 4.9%; + --background: 222 47% 6%; --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; + --card: 222 47% 9%; --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; + --popover: 222 47% 9%; --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: 173 58% 45%; + --primary-foreground: 222 47% 11%; - --secondary: 217.2 32.6% 17.5%; + --secondary: 222 40% 15%; --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; + --muted: 222 40% 15%; + --muted-foreground: 215 20% 65%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --accent: 173 58% 39%; + --accent-foreground: 0 0% 100%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 62% 40%; + --destructive-foreground: 0 0% 100%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; + --success: 152 69% 36%; + --success-foreground: 0 0% 100%; + + --warning: 38 92% 55%; + --warning-foreground: 0 0% 100%; + + --info: 199 89% 53%; + --info-foreground: 0 0% 100%; + + --border: 222 40% 18%; + --input: 222 40% 18%; + --ring: 173 58% 45%; + + --sidebar-background: 222 47% 6%; + --sidebar-foreground: 215 20% 85%; + --sidebar-primary: 173 58% 45%; --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; + --sidebar-accent: 222 40% 12%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 222 40% 15%; + --sidebar-ring: 173 58% 45%; + --sidebar-muted: 215 16% 55%; } } @@ -99,7 +133,102 @@ All colors MUST be HSL. @apply border-border; } + html { + font-family: 'Inter', system-ui, -apple-system, sans-serif; + } + body { - @apply bg-background text-foreground; + @apply bg-background text-foreground antialiased; + font-feature-settings: "cv11", "ss01"; + } + + /* Smooth focus states */ + :focus-visible { + @apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-background; + } +} + +@layer components { + /* Auth card for public pages */ + .auth-card { + @apply bg-card rounded-xl p-8 shadow-card border border-border; + } + + /* Section header styling */ + .section-header { + @apply text-lg font-semibold text-foreground mb-1; + } + + .section-description { + @apply text-sm text-muted-foreground; + } + + /* Status badges */ + .status-badge { + @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; + } + + .status-badge-success { + @apply bg-success/10 text-success; + } + + .status-badge-warning { + @apply bg-warning/10 text-warning; + } + + .status-badge-error { + @apply bg-destructive/10 text-destructive; + } + + .status-badge-info { + @apply bg-info/10 text-info; + } + + /* Page layout helpers */ + .page-container { + @apply p-6 lg:p-8 max-w-5xl; + } + + .page-header { + @apply mb-8; + } + + .page-title { + @apply text-2xl font-semibold text-foreground tracking-tight; + } + + .page-description { + @apply text-muted-foreground mt-1; + } +} + +@layer utilities { + /* Animation utilities */ + .animate-fade-in { + animation: fadeIn 0.3s ease-out; + } + + .animate-slide-in { + animation: slideIn 0.3s ease-out; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes slideIn { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } } } diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 7130b54..1deb5bb 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,14 +1,15 @@ -// Update this page (the content is just a fallback if you fail to update the page) +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; const Index = () => { - return ( -
-
-

Welcome to Your Blank App

-

Start building your amazing project here!

-
-
- ); + const navigate = useNavigate(); + + useEffect(() => { + // Redirect to login for now - will be replaced with auth check + navigate("/login"); + }, [navigate]); + + return null; }; export default Index; diff --git a/src/pages/auth/ForgotPasswordPage.tsx b/src/pages/auth/ForgotPasswordPage.tsx new file mode 100644 index 0000000..55a94bd --- /dev/null +++ b/src/pages/auth/ForgotPasswordPage.tsx @@ -0,0 +1,94 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; +import { Mail, ArrowLeft, ArrowRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export default function ForgotPasswordPage() { + const [email, setEmail] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + setIsSubmitted(true); + }, 1000); + }; + + if (isSubmitted) { + return ( +
+
+ +
+ +

+ Check your email +

+

+ If an account exists for {email}, you'll receive a password reset link shortly. +

+ + + + +
+ ); + } + + return ( +
+
+

+ Forgot password? +

+

+ Enter your email and we'll send you a reset link +

+
+ +
+
+ +
+ + setEmail(e.target.value)} + className="pl-10" + required + /> +
+
+ + +
+ +

+ + + Back to sign in + +

+
+ ); +} diff --git a/src/pages/auth/InviteAcceptPage.tsx b/src/pages/auth/InviteAcceptPage.tsx new file mode 100644 index 0000000..3f8b1d4 --- /dev/null +++ b/src/pages/auth/InviteAcceptPage.tsx @@ -0,0 +1,136 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { User, Lock, Upload, ArrowRight, Building2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; + +export default function InviteAcceptPage() { + const navigate = useNavigate(); + const [name, setName] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + // Mock invite data - will be fetched from URL token + const inviteData = { + email: "invited@example.com", + organization: "Acme Corp", + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirmPassword) { + return; + } + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + navigate("/profile"); + }, 1000); + }; + + return ( +
+
+
+ +
+

+ You're invited! +

+

+ {inviteData.organization} has + invited you to join their organization +

+
+ +
+ {/* Avatar upload */} +
+ + + {name ? name.split(" ").map(n => n[0]).join("").slice(0, 2).toUpperCase() : "?"} + + + +
+ +
+ + +
+ +
+ +
+ + setName(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="pl-10" + required + minLength={8} + /> +
+
+ +
+ +
+ + setConfirmPassword(e.target.value)} + className="pl-10" + required + /> +
+
+ + +
+
+ ); +} diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx new file mode 100644 index 0000000..04ee457 --- /dev/null +++ b/src/pages/auth/LoginPage.tsx @@ -0,0 +1,151 @@ +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { Mail, Lock, ArrowRight, Fingerprint } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; + +export default function LoginPage() { + const navigate = useNavigate(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + // Mock login - will be replaced with actual auth + setTimeout(() => { + setIsLoading(false); + navigate("/profile"); + }, 1000); + }; + + return ( +
+
+

+ Welcome back +

+

+ Sign in to your account to continue +

+
+ +
+
+ +
+ + setEmail(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+
+ + + Forgot password? + +
+
+ + setPassword(e.target.value)} + className="pl-10" + required + /> +
+
+ + +
+ +
+ + + or continue with + +
+ + {/* Alternative login methods */} +
+ + +
+ + + +
+
+ +

+ Don't have an account?{" "} + + Create one + +

+
+ ); +} diff --git a/src/pages/auth/OIDCConsentPage.tsx b/src/pages/auth/OIDCConsentPage.tsx new file mode 100644 index 0000000..2384dcb --- /dev/null +++ b/src/pages/auth/OIDCConsentPage.tsx @@ -0,0 +1,109 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { CheckCircle, XCircle, Shield, User, Mail, Building2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; + +export default function OIDCConsentPage() { + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + + // Mock OIDC client data - will be fetched from auth flow + const clientData = { + name: "GitLab", + logo: null, + redirectUri: "https://gitlab.example.com/callback", + scopes: [ + { id: "openid", name: "OpenID", description: "Verify your identity" }, + { id: "profile", name: "Profile", description: "Access your name and profile picture" }, + { id: "email", name: "Email", description: "Access your email address" }, + ], + }; + + const handleAllow = () => { + setIsLoading(true); + // Mock consent - will redirect to client callback + setTimeout(() => { + setIsLoading(false); + // In real implementation: redirect to redirectUri with auth code + }, 500); + }; + + const handleDeny = () => { + navigate(-1); + }; + + return ( +
+
+
+ +
+

+ Authorize {clientData.name} +

+

+ This application wants to access your account +

+
+ + +

+ {clientData.name} is requesting access to: +

+
    + {clientData.scopes.map((scope) => ( +
  • +
    + {scope.id === "openid" && } + {scope.id === "profile" && } + {scope.id === "email" && } +
    +
    +

    {scope.name}

    +

    {scope.description}

    +
    +
  • + ))} +
+
+ +
+ + + Redirecting to: {clientData.redirectUri} + +
+ + + +
+ + +
+ +

+ You can revoke this access anytime from your{" "} + + linked accounts + +

+
+ ); +} diff --git a/src/pages/auth/OIDCErrorPage.tsx b/src/pages/auth/OIDCErrorPage.tsx new file mode 100644 index 0000000..7ca5311 --- /dev/null +++ b/src/pages/auth/OIDCErrorPage.tsx @@ -0,0 +1,49 @@ +import { AlertTriangle, ArrowLeft, Home } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Link } from "react-router-dom"; + +export default function OIDCErrorPage() { + // Mock error data - will be parsed from URL params + const errorData = { + error: "invalid_request", + description: "The request was missing a required parameter or was otherwise malformed.", + clientName: "Unknown Application", + }; + + return ( +
+
+ +
+ +

+ Authentication Error +

+

+ There was a problem with the authentication request. +

+ +
+

+ Error: {errorData.error} +

+

+ {errorData.description} +

+
+ +
+ + + + +
+
+ ); +} diff --git a/src/pages/auth/RegisterPage.tsx b/src/pages/auth/RegisterPage.tsx new file mode 100644 index 0000000..b9abb8d --- /dev/null +++ b/src/pages/auth/RegisterPage.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { Mail, Lock, User, ArrowRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export default function RegisterPage() { + const navigate = useNavigate(); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirmPassword) { + return; + } + setIsLoading(true); + // Mock registration - will be replaced with actual auth + setTimeout(() => { + setIsLoading(false); + navigate("/verify-email"); + }, 1000); + }; + + return ( +
+
+

+ Create your account +

+

+ Get started with Authy2 in seconds +

+
+ +
+
+ +
+ + setName(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+ +
+ + setEmail(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="pl-10" + required + minLength={8} + /> +
+

+ Must be at least 8 characters +

+
+ +
+ +
+ + setConfirmPassword(e.target.value)} + className="pl-10" + required + /> +
+
+ + +
+ +

+ Already have an account?{" "} + + Sign in + +

+ +

+ By creating an account, you agree to our{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + +

+
+ ); +} diff --git a/src/pages/auth/ResetPasswordPage.tsx b/src/pages/auth/ResetPasswordPage.tsx new file mode 100644 index 0000000..75e2741 --- /dev/null +++ b/src/pages/auth/ResetPasswordPage.tsx @@ -0,0 +1,110 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Lock, ArrowRight, CheckCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export default function ResetPasswordPage() { + const navigate = useNavigate(); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirmPassword) { + return; + } + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + setIsSuccess(true); + }, 1000); + }; + + if (isSuccess) { + return ( +
+
+ +
+ +

+ Password reset successful +

+

+ Your password has been updated. You can now sign in with your new password. +

+ + +
+ ); + } + + return ( +
+
+

+ Reset your password +

+

+ Enter a new password for your account +

+
+ +
+
+ +
+ + setPassword(e.target.value)} + className="pl-10" + required + minLength={8} + /> +
+

+ Must be at least 8 characters +

+
+ +
+ +
+ + setConfirmPassword(e.target.value)} + className="pl-10" + required + /> +
+
+ + +
+
+ ); +} diff --git a/src/pages/auth/VerifyEmailPage.tsx b/src/pages/auth/VerifyEmailPage.tsx new file mode 100644 index 0000000..97ad4d0 --- /dev/null +++ b/src/pages/auth/VerifyEmailPage.tsx @@ -0,0 +1,34 @@ +import { Mail, RefreshCw } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Link } from "react-router-dom"; + +export default function VerifyEmailPage() { + return ( +
+
+ +
+ +

+ Check your email +

+

+ We've sent a verification link to your email address. Click the link to verify your account. +

+ +
+ +
+ +

+ Wrong email?{" "} + + Go back + +

+
+ ); +} diff --git a/src/pages/org/MembersPage.tsx b/src/pages/org/MembersPage.tsx new file mode 100644 index 0000000..82507af --- /dev/null +++ b/src/pages/org/MembersPage.tsx @@ -0,0 +1,210 @@ +import { useState } from "react"; +import { Search, Plus, MoreHorizontal, Shield, User, Mail, Clock } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +const members = [ + { + id: "1", + name: "John Doe", + email: "john@example.com", + role: "admin", + status: "active", + lastActive: "2 hours ago", + avatar: null, + initials: "JD", + }, + { + id: "2", + name: "Jane Smith", + email: "jane@example.com", + role: "member", + status: "active", + lastActive: "5 minutes ago", + avatar: null, + initials: "JS", + }, + { + id: "3", + name: "Bob Wilson", + email: "bob@example.com", + role: "member", + status: "disabled", + lastActive: "3 days ago", + avatar: null, + initials: "BW", + }, +]; + +const pendingInvites = [ + { + id: "1", + email: "alice@example.com", + role: "member", + sentAt: "2 days ago", + expiresAt: "5 days", + }, + { + id: "2", + email: "charlie@example.com", + role: "admin", + sentAt: "1 hour ago", + expiresAt: "7 days", + }, +]; + +export default function MembersPage() { + const [search, setSearch] = useState(""); + + return ( +
+
+
+

Members

+

+ Manage organization members and invitations +

+
+ +
+ + + + + Members ({members.length}) + + + Pending Invites ({pendingInvites.length}) + + + + +
+
+ + setSearch(e.target.value)} + className="pl-10 max-w-sm" + /> +
+
+ + + +
+ {members.map((member) => ( +
+ + + + {member.initials} + + +
+
+

+ {member.name} +

+ {member.role === "admin" && ( + + + Admin + + )} + {member.status === "disabled" && ( + + Disabled + + )} +
+

+ {member.email} +

+
+

+ Active {member.lastActive} +

+ + + + + + + + View profile + + + + Change role + + + + {member.status === "active" ? "Disable" : "Enable"} account + + + +
+ ))} +
+
+
+
+ + + + +
+ {pendingInvites.map((invite) => ( +
+
+ +
+
+

+ {invite.email} +

+
+ Invited as {invite.role} + + + + Expires in {invite.expiresAt} + +
+
+
+ + +
+
+ ))} +
+
+
+
+
+
+ ); +} diff --git a/src/pages/org/OIDCClientsPage.tsx b/src/pages/org/OIDCClientsPage.tsx new file mode 100644 index 0000000..364cfff --- /dev/null +++ b/src/pages/org/OIDCClientsPage.tsx @@ -0,0 +1,177 @@ +import { useState } from "react"; +import { Plus, Key, ExternalLink, MoreHorizontal, Copy, RefreshCw, Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; + +const clients = [ + { + id: "1", + name: "GitLab", + clientId: "gitlab_prod_xxxxxxxxxxxxx", + redirectUris: ["https://gitlab.example.com/callback"], + scopes: ["openid", "profile", "email"], + createdAt: "2024-01-10", + lastUsed: "2 hours ago", + }, + { + id: "2", + name: "Grafana", + clientId: "grafana_prod_xxxxxxxxxxxxx", + redirectUris: ["https://grafana.example.com/login/generic_oauth"], + scopes: ["openid", "profile"], + createdAt: "2024-01-08", + lastUsed: "5 minutes ago", + }, + { + id: "3", + name: "OAuth2 Proxy", + clientId: "oauth2proxy_xxxxxxxxxxxxx", + redirectUris: ["https://auth.example.com/oauth2/callback"], + scopes: ["openid", "profile", "email", "groups"], + createdAt: "2024-01-05", + lastUsed: "1 day ago", + }, +]; + +export default function OIDCClientsPage() { + const [isCreateOpen, setIsCreateOpen] = useState(false); + + return ( +
+
+
+

OIDC Clients

+

+ Manage applications that authenticate via Authy2 +

+
+ + + + + + + Create OIDC Client + + Register a new application to authenticate via Authy2 + + +
+
+ + +
+
+ +