From 0104839c118f2a57f1da83aa0249bf61401a609d Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Tue, 6 Jan 2026 14:46:23 +0000
Subject: [PATCH] Changes
---
src/App.tsx | 64 ++++-
.../layouts/AuthenticatedLayout.tsx | 20 ++
src/components/layouts/PublicLayout.tsx | 39 +++
src/components/navigation/AppSidebar.tsx | 176 +++++++++++++
src/components/navigation/TopBar.tsx | 127 ++++++++++
src/index.css | 239 ++++++++++++++----
src/pages/Index.tsx | 19 +-
src/pages/auth/ForgotPasswordPage.tsx | 94 +++++++
src/pages/auth/InviteAcceptPage.tsx | 136 ++++++++++
src/pages/auth/LoginPage.tsx | 151 +++++++++++
src/pages/auth/OIDCConsentPage.tsx | 109 ++++++++
src/pages/auth/OIDCErrorPage.tsx | 49 ++++
src/pages/auth/RegisterPage.tsx | 140 ++++++++++
src/pages/auth/ResetPasswordPage.tsx | 110 ++++++++
src/pages/auth/VerifyEmailPage.tsx | 34 +++
src/pages/org/MembersPage.tsx | 210 +++++++++++++++
src/pages/org/OIDCClientsPage.tsx | 177 +++++++++++++
src/pages/org/OrgAuditPage.tsx | 177 +++++++++++++
src/pages/org/OrgOverviewPage.tsx | 124 +++++++++
src/pages/org/PoliciesPage.tsx | 163 ++++++++++++
src/pages/user/ActivityPage.tsx | 182 +++++++++++++
src/pages/user/LinkedAccountsPage.tsx | 121 +++++++++
src/pages/user/ProfilePage.tsx | 150 +++++++++++
src/pages/user/SecurityPage.tsx | 185 ++++++++++++++
tailwind.config.ts | 38 ++-
25 files changed, 2955 insertions(+), 79 deletions(-)
create mode 100644 src/components/layouts/AuthenticatedLayout.tsx
create mode 100644 src/components/layouts/PublicLayout.tsx
create mode 100644 src/components/navigation/AppSidebar.tsx
create mode 100644 src/components/navigation/TopBar.tsx
create mode 100644 src/pages/auth/ForgotPasswordPage.tsx
create mode 100644 src/pages/auth/InviteAcceptPage.tsx
create mode 100644 src/pages/auth/LoginPage.tsx
create mode 100644 src/pages/auth/OIDCConsentPage.tsx
create mode 100644 src/pages/auth/OIDCErrorPage.tsx
create mode 100644 src/pages/auth/RegisterPage.tsx
create mode 100644 src/pages/auth/ResetPasswordPage.tsx
create mode 100644 src/pages/auth/VerifyEmailPage.tsx
create mode 100644 src/pages/org/MembersPage.tsx
create mode 100644 src/pages/org/OIDCClientsPage.tsx
create mode 100644 src/pages/org/OrgAuditPage.tsx
create mode 100644 src/pages/org/OrgOverviewPage.tsx
create mode 100644 src/pages/org/PoliciesPage.tsx
create mode 100644 src/pages/user/ActivityPage.tsx
create mode 100644 src/pages/user/LinkedAccountsPage.tsx
create mode 100644 src/pages/user/ProfilePage.tsx
create mode 100644 src/pages/user/SecurityPage.tsx
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 */}
+
+
+ {/* Main content */}
+
+
+
+
+
+
+ {/* Footer */}
+
+
+ );
+}
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 (
+
+ );
+}
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.
+
+
+
+
+
+ Back to sign in
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Forgot password?
+
+
+ Enter your email and we'll send you a reset link
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+ );
+}
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
+
+
+
+
+
+
+
+
+ or continue with
+
+
+
+ {/* Alternative login methods */}
+
+
+
+ Sign in with Passkey
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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:
+
+
+
+
+
+
+
+ Redirecting to: {clientData.redirectUri}
+
+
+
+
+
+
+
+
+ Deny
+
+
+
+ {isLoading ? "Authorizing..." : "Allow"}
+
+
+
+
+ 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}
+
+
+
+
+
window.history.back()}>
+
+ Go back
+
+
+
+
+ Return to home
+
+
+
+
+ );
+}
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
+
+
+
+
+
+
+ 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.
+
+
+
navigate("/login")} className="w-full">
+ Sign in
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Reset your password
+
+
+ Enter a new password for your account
+
+
+
+
+
+ );
+}
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.
+
+
+
+
+
+ Resend verification email
+
+
+
+
+ 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
+
+
+
+
+ Invite member
+
+
+
+
+
+
+ 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}
+
+
+
+
+
+ Resend
+
+
+ Revoke
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
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
+
+
+
+
+
+
+ Add client
+
+
+
+
+ Create OIDC Client
+
+ Register a new application to authenticate via Authy2
+
+
+
+
+ Client name
+
+
+
+
Redirect URIs
+
+
+ One URI per line. These are the allowed callback URLs.
+
+
+
+ setIsCreateOpen(false)}>
+ Cancel
+
+ setIsCreateOpen(false)}>
+ Create client
+
+
+
+
+
+
+
+
+ {clients.map((client) => (
+
+
+
+
+
+
+
+
+
{client.name}
+
+
+ {client.clientId}
+
+
+
+
+
+
+ {client.scopes.map((scope) => (
+
+ {scope}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ View details
+
+
+
+ Rotate secret
+
+
+
+
+ Delete client
+
+
+
+
+
+
+ Created {client.createdAt}
+ •
+ Last used {client.lastUsed}
+
+
+ {client.redirectUris.length} redirect URI{client.redirectUris.length > 1 ? "s" : ""}
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/pages/org/OrgAuditPage.tsx b/src/pages/org/OrgAuditPage.tsx
new file mode 100644
index 0000000..2105d69
--- /dev/null
+++ b/src/pages/org/OrgAuditPage.tsx
@@ -0,0 +1,177 @@
+import { useState } from "react";
+import { Search, Filter, Download, User, Settings, Key, UserPlus, AlertTriangle } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+
+const auditEvents = [
+ {
+ id: "1",
+ type: "member_invited",
+ actor: "John Doe",
+ target: "alice@example.com",
+ timestamp: "2024-01-15T10:30:00Z",
+ details: "Invited as member",
+ },
+ {
+ id: "2",
+ type: "policy_changed",
+ actor: "John Doe",
+ target: "Password Policy",
+ timestamp: "2024-01-15T09:00:00Z",
+ details: "Minimum length changed from 8 to 12",
+ },
+ {
+ id: "3",
+ type: "member_disabled",
+ actor: "Jane Smith",
+ target: "bob@example.com",
+ timestamp: "2024-01-14T15:45:00Z",
+ details: "Account disabled",
+ },
+ {
+ id: "4",
+ type: "client_created",
+ actor: "John Doe",
+ target: "GitLab",
+ timestamp: "2024-01-14T12:00:00Z",
+ details: "OIDC client created",
+ },
+ {
+ id: "5",
+ type: "role_changed",
+ actor: "John Doe",
+ target: "jane@example.com",
+ timestamp: "2024-01-13T09:00:00Z",
+ details: "Role changed from member to admin",
+ },
+];
+
+const getEventIcon = (type: string) => {
+ switch (type) {
+ case "member_invited":
+ case "role_changed":
+ return ;
+ case "policy_changed":
+ return ;
+ case "member_disabled":
+ return ;
+ case "client_created":
+ return ;
+ default:
+ return ;
+ }
+};
+
+const getEventTitle = (type: string) => {
+ switch (type) {
+ case "member_invited":
+ return "Member invited";
+ case "policy_changed":
+ return "Policy changed";
+ case "member_disabled":
+ return "Member disabled";
+ case "client_created":
+ return "OIDC client created";
+ case "role_changed":
+ return "Role changed";
+ default:
+ return "Event";
+ }
+};
+
+export default function OrgAuditPage() {
+ const [search, setSearch] = useState("");
+ const [typeFilter, setTypeFilter] = useState("all");
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return new Intl.DateTimeFormat("en-US", {
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ }).format(date);
+ };
+
+ return (
+
+
+
+
Audit Log
+
+ View all administrative actions and changes
+
+
+
+
+ Export
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+
+
+
+ All events
+ Member changes
+ Policy changes
+ OIDC clients
+
+
+
+
+
+
+
+ {auditEvents.map((event) => (
+
+
+ {getEventIcon(event.type)}
+
+
+
+
+ {getEventTitle(event.type)}
+
+
+ {event.target}
+
+
+
+ by {event.actor}
+ •
+ {event.details}
+
+
+
+ {formatDate(event.timestamp)}
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/pages/org/OrgOverviewPage.tsx b/src/pages/org/OrgOverviewPage.tsx
new file mode 100644
index 0000000..c009095
--- /dev/null
+++ b/src/pages/org/OrgOverviewPage.tsx
@@ -0,0 +1,124 @@
+import { Building2, Users, Shield, Key, ArrowRight, TrendingUp } from "lucide-react";
+import { Link } from "react-router-dom";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+
+export default function OrgOverviewPage() {
+ // Mock organization data
+ const org = {
+ name: "Acme Corp",
+ createdAt: "January 2024",
+ stats: {
+ totalMembers: 24,
+ activeToday: 18,
+ pendingInvites: 3,
+ oidcClients: 5,
+ },
+ };
+
+ const quickLinks = [
+ {
+ title: "Members",
+ description: "Manage team members and roles",
+ icon: Users,
+ href: "/org/members",
+ },
+ {
+ title: "Policies",
+ description: "Configure security requirements",
+ icon: Shield,
+ href: "/org/policies",
+ },
+ {
+ title: "OIDC Clients",
+ description: "Manage connected applications",
+ icon: Key,
+ href: "/org/clients",
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
{org.name}
+
Created {org.createdAt}
+
+
+
+
+ {/* Stats */}
+
+
+
+
+
+
Total Members
+
{org.stats.totalMembers}
+
+
+
+
+
+
+
+
+
+
Active Today
+
{org.stats.activeToday}
+
+
+
+
+
+
+
+
+
+
Pending Invites
+
{org.stats.pendingInvites}
+
+
+
+
+
+
+
+
+
+
OIDC Clients
+
{org.stats.oidcClients}
+
+
+
+
+
+
+
+ {/* Quick Links */}
+
Quick Actions
+
+ {quickLinks.map((link) => (
+
+
+
+
+ {link.title}
+ {link.description}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/pages/org/PoliciesPage.tsx b/src/pages/org/PoliciesPage.tsx
new file mode 100644
index 0000000..347ee92
--- /dev/null
+++ b/src/pages/org/PoliciesPage.tsx
@@ -0,0 +1,163 @@
+import { Shield, Lock, Fingerprint, Smartphone, UserPlus, AlertTriangle } from "lucide-react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Slider } from "@/components/ui/slider";
+import { Badge } from "@/components/ui/badge";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+
+export default function PoliciesPage() {
+ return (
+
+
+
Security Policies
+
+ Configure security requirements for organization members
+
+
+
+
+ {/* Registration Mode */}
+
+
+
+
+ Registration Mode
+
+
+ Control how new members can join your organization
+
+
+
+
+
+
+
+
+ Open registration
+ Invite only
+ Closed
+
+
+
+ Invite only: Members can only join via admin invitation
+
+
+
+
+ {/* Password Policy */}
+
+
+
+
+ Password Policy
+
+
+ Set minimum password requirements for all members
+
+
+
+
+
Minimum password length
+
+
+ 12 chars
+
+
+
+
+
+
+
Require uppercase letters
+
At least one A-Z
+
+
+
+
+
+
+
Require numbers
+
At least one 0-9
+
+
+
+
+
+
+
Require special characters
+
At least one !@#$%^&*
+
+
+
+
+
+
+
+ {/* MFA Requirements */}
+
+
+
+
+ Multi-Factor Authentication
+
+
+ Require additional authentication methods
+
+
+
+
+
+
+ Require TOTP
+ Recommended
+
+
+ All members must set up an authenticator app
+
+
+
+
+
+
+
+
+ Enabling this will require all existing members to set up TOTP on their next login.
+
+
+
+
+
+ {/* Passkey Requirements */}
+
+
+
+
+ Passkeys (WebAuthn)
+
+
+ Require passwordless authentication capability
+
+
+
+
+
+
Require at least one passkey
+
+ Members must register a passkey for backup authentication
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/user/ActivityPage.tsx b/src/pages/user/ActivityPage.tsx
new file mode 100644
index 0000000..a68c42c
--- /dev/null
+++ b/src/pages/user/ActivityPage.tsx
@@ -0,0 +1,182 @@
+import { useState } from "react";
+import { LogIn, LogOut, Key, Fingerprint, Smartphone, AlertTriangle, CheckCircle, MapPin } from "lucide-react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+
+const activityEvents = [
+ {
+ id: "1",
+ type: "login_success",
+ method: "password",
+ timestamp: "2024-01-15T10:30:00Z",
+ location: "San Francisco, CA",
+ device: "Chrome on macOS",
+ ip: "192.168.1.1",
+ },
+ {
+ id: "2",
+ type: "login_success",
+ method: "passkey",
+ timestamp: "2024-01-14T15:45:00Z",
+ location: "San Francisco, CA",
+ device: "Safari on iOS",
+ ip: "192.168.1.2",
+ },
+ {
+ id: "3",
+ type: "login_failed",
+ method: "password",
+ timestamp: "2024-01-14T12:00:00Z",
+ location: "Unknown",
+ device: "Firefox on Windows",
+ ip: "10.0.0.5",
+ },
+ {
+ id: "4",
+ type: "mfa_enabled",
+ method: "totp",
+ timestamp: "2024-01-13T09:00:00Z",
+ location: "San Francisco, CA",
+ device: "Chrome on macOS",
+ ip: "192.168.1.1",
+ },
+ {
+ id: "5",
+ type: "passkey_added",
+ method: "passkey",
+ timestamp: "2024-01-12T14:30:00Z",
+ location: "San Francisco, CA",
+ device: "Safari on macOS",
+ ip: "192.168.1.1",
+ },
+];
+
+const getEventIcon = (type: string, method: string) => {
+ switch (type) {
+ case "login_success":
+ return method === "passkey" ? (
+
+ ) : (
+
+ );
+ case "login_failed":
+ return ;
+ case "mfa_enabled":
+ return ;
+ case "passkey_added":
+ return ;
+ case "logout":
+ return ;
+ default:
+ return ;
+ }
+};
+
+const getEventTitle = (type: string, method: string) => {
+ switch (type) {
+ case "login_success":
+ return `Signed in with ${method}`;
+ case "login_failed":
+ return "Failed login attempt";
+ case "mfa_enabled":
+ return "Two-factor authentication enabled";
+ case "passkey_added":
+ return "Passkey added";
+ case "logout":
+ return "Signed out";
+ default:
+ return "Security event";
+ }
+};
+
+const getEventStatus = (type: string) => {
+ if (type === "login_failed") {
+ return { variant: "destructive" as const, label: "Failed" };
+ }
+ return { variant: "default" as const, label: "Success" };
+};
+
+export default function ActivityPage() {
+ const [filter, setFilter] = useState("all");
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return new Intl.DateTimeFormat("en-US", {
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ }).format(date);
+ };
+
+ return (
+
+
+
+
Activity
+
+ Your recent account activity and security events
+
+
+
+
+
+
+
+ All events
+ Logins only
+ Security changes
+
+
+
+
+
+
+
+ {activityEvents.map((event) => {
+ const status = getEventStatus(event.type);
+ return (
+
+
+ {getEventIcon(event.type, event.method)}
+
+
+
+
+ {getEventTitle(event.type, event.method)}
+
+ {event.type === "login_failed" && (
+
+ Failed
+
+ )}
+
+
+
{event.device}
+
+
+ {event.location}
+ •
+ {event.ip}
+
+
+
+
+ {formatDate(event.timestamp)}
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/src/pages/user/LinkedAccountsPage.tsx b/src/pages/user/LinkedAccountsPage.tsx
new file mode 100644
index 0000000..c47a97a
--- /dev/null
+++ b/src/pages/user/LinkedAccountsPage.tsx
@@ -0,0 +1,121 @@
+import { Link2, Unlink, AlertCircle } 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 { Alert, AlertDescription } from "@/components/ui/alert";
+
+const socialProviders = [
+ {
+ id: "google",
+ name: "Google",
+ icon: (
+
+
+
+
+
+
+ ),
+ linked: true,
+ email: "john.doe@gmail.com",
+ },
+ {
+ id: "github",
+ name: "GitHub",
+ icon: (
+
+
+
+ ),
+ linked: true,
+ email: "johndoe",
+ },
+ {
+ id: "microsoft",
+ name: "Microsoft",
+ icon: (
+
+
+
+
+
+
+ ),
+ linked: false,
+ email: null,
+ },
+];
+
+export default function LinkedAccountsPage() {
+ return (
+
+
+
Linked Accounts
+
+ Connect external accounts for alternative login methods
+
+
+
+
+
+
+ Linked accounts can only be used to sign in to an existing Authy2 account.
+ They cannot be used to create new accounts.
+
+
+
+
+ {socialProviders.map((provider) => (
+
+
+
+
+
+ {provider.icon}
+
+
+
{provider.name}
+ {provider.linked ? (
+
{provider.email}
+ ) : (
+
Not connected
+ )}
+
+
+ {provider.linked ? (
+
+ Connected
+
+
+ Disconnect
+
+
+ ) : (
+
+
+ Connect
+
+ )}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/pages/user/ProfilePage.tsx b/src/pages/user/ProfilePage.tsx
new file mode 100644
index 0000000..de01bac
--- /dev/null
+++ b/src/pages/user/ProfilePage.tsx
@@ -0,0 +1,150 @@
+import { useState } from "react";
+import { Mail, Building2, Upload, CheckCircle, AlertCircle } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+
+export default function ProfilePage() {
+ const [name, setName] = useState("John Doe");
+ const [isEditing, setIsEditing] = useState(false);
+
+ // Mock user data
+ const user = {
+ name: "John Doe",
+ email: "john@example.com",
+ emailVerified: true,
+ avatar: null,
+ initials: "JD",
+ organizations: [
+ { name: "Acme Corp", role: "Admin" },
+ { name: "Beta Inc", role: "Member" },
+ ],
+ };
+
+ const handleSave = () => {
+ setIsEditing(false);
+ // Save logic here
+ };
+
+ return (
+
+
+
Profile
+
+ Manage your personal information and organization memberships
+
+
+
+
+ {/* Profile Photo & Name */}
+
+
+ Personal Information
+ Update your photo and personal details
+
+
+ {/* Avatar */}
+
+
+
+
+ {user.initials}
+
+
+
+
+
+ Change photo
+
+
+ JPG, PNG or GIF. Max 2MB.
+
+
+
+
+ {/* Name */}
+
+
Full name
+ {isEditing ? (
+
+ setName(e.target.value)}
+ />
+ Save
+ setIsEditing(false)}>
+ Cancel
+
+
+ ) : (
+
+ {user.name}
+ setIsEditing(true)}>
+ Edit
+
+
+ )}
+
+
+
+
+ {/* Email */}
+
+
+ Email Address
+ Your email is used for login and notifications
+
+
+
+
+
+
{user.email}
+ {user.emailVerified ? (
+
+
+ Verified
+
+ ) : (
+
+
+ Unverified
+
+ )}
+
+
+
+
+
+ {/* Organizations */}
+
+
+ Organizations
+ Organizations you're a member of
+
+
+
+ {user.organizations.map((org, index) => (
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/pages/user/SecurityPage.tsx b/src/pages/user/SecurityPage.tsx
new file mode 100644
index 0000000..7347fae
--- /dev/null
+++ b/src/pages/user/SecurityPage.tsx
@@ -0,0 +1,185 @@
+import { useState } from "react";
+import { Lock, Fingerprint, Smartphone, Shield, Plus, AlertTriangle, CheckCircle } from "lucide-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 { Badge } from "@/components/ui/badge";
+import { Switch } from "@/components/ui/switch";
+
+export default function SecurityPage() {
+ const [showPasswordForm, setShowPasswordForm] = useState(false);
+
+ // Mock security data
+ const security = {
+ passwordLastChanged: "3 months ago",
+ totpEnabled: true,
+ passkeysCount: 2,
+ passkeys: [
+ { id: "1", name: "MacBook Pro Touch ID", lastUsed: "Today" },
+ { id: "2", name: "iPhone Face ID", lastUsed: "Yesterday" },
+ ],
+ policyRequirements: {
+ totpRequired: true,
+ passkeysRequired: false,
+ minPasswordLength: 12,
+ },
+ };
+
+ return (
+
+
+
Security
+
+ Manage your authentication methods and security settings
+
+
+
+
+ {/* Policy Status */}
+
+
+
+
+
+
Organization Policy
+
+ Your organization requires TOTP to be enabled for all members.
+
+
+
+
+
+
+ {/* Password */}
+
+
+
+
+
+
+ Password
+
+ Last changed {security.passwordLastChanged}
+
+
setShowPasswordForm(!showPasswordForm)}
+ >
+ Change password
+
+
+
+ {showPasswordForm && (
+
+
+ Current password
+
+
+
+
New password
+
+
+ Minimum {security.policyRequirements.minPasswordLength} characters required by organization
+
+
+
+ Confirm new password
+
+
+
+ Update password
+ setShowPasswordForm(false)}>
+ Cancel
+
+
+
+ )}
+
+
+ {/* TOTP / Authenticator */}
+
+
+
+
+
+
+ Authenticator App (TOTP)
+ {security.policyRequirements.totpRequired && (
+ Required
+ )}
+
+
+ Use an authenticator app for two-factor authentication
+
+
+ {security.totpEnabled ? (
+
+
+ Enabled
+
+ ) : (
+
Set up
+ )}
+
+
+ {security.totpEnabled && (
+
+
+ Reconfigure
+
+
+ )}
+
+
+ {/* Passkeys */}
+
+
+
+
+
+
+ Passkeys
+ {security.policyRequirements.passkeysRequired && (
+ Required
+ )}
+
+
+ Use biometrics or security keys for passwordless login
+
+
+
+
+ Add passkey
+
+
+
+
+
+ {security.passkeys.map((passkey) => (
+
+
+
+
+
+
+
{passkey.name}
+
Last used: {passkey.lastUsed}
+
+
+
+ Remove
+
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
index a1edb69..6ac1687 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -13,6 +13,9 @@ export default {
},
},
extend: {
+ fontFamily: {
+ sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
+ },
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
@@ -31,6 +34,18 @@ export default {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
+ success: {
+ DEFAULT: "hsl(var(--success))",
+ foreground: "hsl(var(--success-foreground))",
+ },
+ warning: {
+ DEFAULT: "hsl(var(--warning))",
+ foreground: "hsl(var(--warning-foreground))",
+ },
+ info: {
+ DEFAULT: "hsl(var(--info))",
+ foreground: "hsl(var(--info-foreground))",
+ },
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
@@ -56,6 +71,7 @@ export default {
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
+ muted: "hsl(var(--sidebar-muted))",
},
},
borderRadius: {
@@ -63,22 +79,20 @@ export default {
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
+ boxShadow: {
+ 'sm': 'var(--shadow-sm)',
+ 'md': 'var(--shadow-md)',
+ 'lg': 'var(--shadow-lg)',
+ 'card': 'var(--shadow-card)',
+ },
keyframes: {
"accordion-down": {
- from: {
- height: "0",
- },
- to: {
- height: "var(--radix-accordion-content-height)",
- },
+ from: { height: "0" },
+ to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
- from: {
- height: "var(--radix-accordion-content-height)",
- },
- to: {
- height: "0",
- },
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: "0" },
},
},
animation: {