mirror of
https://github.com/Nezumi-2711/s3-drive-storage-manage.git
synced 2026-09-22 13:48:31 +00:00
feat: init the project
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/* App styles */
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { AuthProvider } from "@/contexts/AuthContext"
|
||||
import { AppRouter } from "@/router"
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<AppRouter />
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,25 @@
|
||||
import React from "react"
|
||||
import { Navigate } from "react-router"
|
||||
import { useAuth } from "@/hooks/useAuth"
|
||||
import { Loader2 } from "lucide-react"
|
||||
|
||||
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated, isLoading } = useAuth()
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<p className="text-sm text-muted-foreground">Checking authentication...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/sign-in" replace />
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 cursor-pointer active:scale-[0.98]",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-gradient-to-r from-blue-600 via-indigo-600 to-cyan-600 text-white shadow-md shadow-blue-500/25 hover:shadow-lg hover:shadow-blue-500/35 hover:brightness-110",
|
||||
destructive:
|
||||
"bg-gradient-to-r from-red-600 to-rose-600 text-white shadow-sm shadow-red-500/20 hover:bg-destructive/90 hover:shadow-md hover:shadow-red-500/30",
|
||||
outline:
|
||||
"border border-border/80 bg-card/60 backdrop-blur-sm shadow-xs hover:bg-accent/60 hover:text-accent-foreground hover:border-border",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent/60 hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-8 rounded-lg px-3 text-xs",
|
||||
lg: "h-11 rounded-xl px-8 text-base",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants, type ButtonProps } from "./button-variants"
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button }
|
||||
export type { ButtonProps } from "./button-variants"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn("flex flex-col gap-1.5 px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface LabelProps
|
||||
extends React.LabelHTMLAttributes<HTMLLabelElement> {}
|
||||
|
||||
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 select-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Label.displayName = "Label"
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import {
|
||||
isAuthenticated as checkIsAuthenticated,
|
||||
login as authLogin,
|
||||
logout as authLogout,
|
||||
type AuthResult,
|
||||
} from "@/lib/auth"
|
||||
import { AuthContext } from "./auth-context-def"
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Check initial auth state from localStorage
|
||||
setIsAuthenticated(checkIsAuthenticated())
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
const signIn = async (password: string): Promise<AuthResult> => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const result = await authLogin(password)
|
||||
if (result.success) {
|
||||
setIsAuthenticated(true)
|
||||
}
|
||||
return result
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const signOut = () => {
|
||||
authLogout()
|
||||
setIsAuthenticated(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
signIn,
|
||||
signOut,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export type { AuthContextType } from "./auth-context-def"
|
||||
export { AuthContext } from "./auth-context-def"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createContext } from "react"
|
||||
import type { AuthResult } from "@/lib/auth"
|
||||
|
||||
export interface AuthContextType {
|
||||
isAuthenticated: boolean
|
||||
isLoading: boolean
|
||||
signIn: (password: string) => Promise<AuthResult>
|
||||
signOut: () => void
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react"
|
||||
import { AuthContext, type AuthContextType } from "@/contexts/auth-context-def"
|
||||
|
||||
export function useAuth(): AuthContextType {
|
||||
const context = useContext(AuthContext)
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 210 40% 98%;
|
||||
--foreground: 222 47% 11%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222 47% 11%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222 47% 11%;
|
||||
--primary: 221 83% 53%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 214 32% 94%;
|
||||
--secondary-foreground: 222 47% 15%;
|
||||
--muted: 214 32% 93%;
|
||||
--muted-foreground: 215 16% 47%;
|
||||
--accent: 214 32% 90%;
|
||||
--accent-foreground: 221 83% 45%;
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 214 32% 88%;
|
||||
--input: 214 32% 88%;
|
||||
--ring: 221 83% 53%;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71% 4%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222 47% 8%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222 47% 8%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217 91% 60%;
|
||||
--primary-foreground: 222 47% 11%;
|
||||
--secondary: 217 33% 17%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217 33% 17%;
|
||||
--muted-foreground: 215 20% 65%;
|
||||
--accent: 217 33% 20%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 40.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 217 33% 20%;
|
||||
--input: 217 33% 20%;
|
||||
--ring: 217 91% 60%;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
font-family: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Custom subtle mesh & accent utilities */
|
||||
.bg-grid-pattern {
|
||||
background-size: 32px 32px;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(99, 102, 241, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(99, 102, 241, 0.05) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.bg-gradient-brand {
|
||||
background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
|
||||
}
|
||||
|
||||
.text-gradient-brand {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #06b6d4 50%, #10b981 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { hashPassword, setAuthToken, type AuthResult } from "./auth"
|
||||
|
||||
// Default mock password if not set in .env
|
||||
const MOCK_ENV_PASSWORD = import.meta.env.VITE_MOCK_PASSWORD || "admin123"
|
||||
|
||||
/**
|
||||
* Mock login function for development when backend is not connected.
|
||||
* Verifies the incoming password hash against the hash of VITE_MOCK_PASSWORD.
|
||||
*/
|
||||
export async function mockLogin(incomingPasswordHash: string): Promise<AuthResult> {
|
||||
// Simulate network latency
|
||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||
|
||||
const expectedHash = await hashPassword(MOCK_ENV_PASSWORD)
|
||||
|
||||
if (incomingPasswordHash === expectedHash) {
|
||||
const mockToken = `mock_token_${Date.now()}_${Math.random().toString(36).substring(2)}`
|
||||
setAuthToken(mockToken)
|
||||
return { success: true, token: mockToken }
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Incorrect password. Please try again.",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { mockLogin } from "./auth-mock"
|
||||
|
||||
export const AUTH_STORAGE_KEY = "s3_drive_storage_auth_token"
|
||||
export const API_BASE_URL = import.meta.env.VITE_API_URL || ""
|
||||
|
||||
export interface AuthResult {
|
||||
success: boolean
|
||||
token?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a plain-text password to a SHA-256 hexadecimal string using the browser's Web Crypto API.
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(password)
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform login by hashing the password and validating it against the API (or mock).
|
||||
*/
|
||||
export async function login(password: string): Promise<AuthResult> {
|
||||
if (!password.trim()) {
|
||||
return { success: false, error: "Password cannot be empty" }
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password)
|
||||
|
||||
// If no API_BASE_URL configured or in mock mode, use mock authentication
|
||||
if (!API_BASE_URL) {
|
||||
return mockLogin(hashedPassword)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/verify`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ passwordHash: hashedPassword }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
error: data.message || "Invalid password",
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const token = data.token || "authenticated"
|
||||
setAuthToken(token)
|
||||
return { success: true, token }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Failed to connect to authentication service",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the auth token in localStorage.
|
||||
*/
|
||||
export function setAuthToken(token: string): void {
|
||||
localStorage.setItem(AUTH_STORAGE_KEY, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the auth token from localStorage.
|
||||
*/
|
||||
export function getAuthToken(): string | null {
|
||||
return localStorage.getItem(AUTH_STORAGE_KEY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the auth token from localStorage.
|
||||
*/
|
||||
export function logout(): void {
|
||||
localStorage.removeItem(AUTH_STORAGE_KEY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is currently authenticated.
|
||||
*/
|
||||
export function isAuthenticated(): boolean {
|
||||
return !!getAuthToken()
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,296 @@
|
||||
import { useNavigate } from "react-router"
|
||||
import {
|
||||
Database,
|
||||
LogOut,
|
||||
ShieldCheck,
|
||||
HardDrive,
|
||||
Layers,
|
||||
Server,
|
||||
Activity,
|
||||
FolderTree,
|
||||
UploadCloud,
|
||||
CheckCircle2,
|
||||
Lock,
|
||||
ArrowUpRight,
|
||||
Radio,
|
||||
Cpu,
|
||||
Sparkles,
|
||||
} from "lucide-react"
|
||||
import { useAuth } from "@/hooks/useAuth"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
|
||||
export default function Dashboard() {
|
||||
const { signOut } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleSignOut = () => {
|
||||
signOut()
|
||||
navigate("/sign-in", { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen bg-background bg-grid-pattern flex flex-col overflow-x-hidden">
|
||||
{/* Background ambient colorful glow effects */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-[15%] -left-[10%] h-[500px] w-[500px] rounded-full bg-blue-500/10 blur-[130px]" />
|
||||
<div className="absolute top-[20%] -right-[15%] h-[550px] w-[550px] rounded-full bg-cyan-400/10 blur-[140px]" />
|
||||
<div className="absolute -bottom-[20%] left-[30%] h-[400px] w-[400px] rounded-full bg-indigo-500/10 blur-[130px]" />
|
||||
</div>
|
||||
|
||||
{/* Navigation Header */}
|
||||
<header className="border-b border-border/70 bg-card/80 backdrop-blur-md sticky top-0 z-20 shadow-xs">
|
||||
<div className="max-w-6xl mx-auto px-3 sm:px-6 min-h-16 py-2.5 sm:py-0 flex items-center justify-between gap-3">
|
||||
{/* Brand Info */}
|
||||
<div className="flex items-center gap-2.5 sm:gap-3 min-w-0">
|
||||
<div className="relative shrink-0">
|
||||
<div className="h-9 w-9 sm:h-10 sm:w-10 rounded-xl bg-gradient-to-tr from-blue-600 via-indigo-600 to-cyan-500 text-white flex items-center justify-center shadow-md shadow-blue-500/20">
|
||||
<Database className="h-4.5 w-4.5 sm:h-5 sm:w-5 stroke-[2.2]" />
|
||||
</div>
|
||||
<span className="absolute -bottom-0.5 -right-0.5 flex h-2.5 w-2.5 sm:h-3 sm:w-3">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-full w-full bg-emerald-500 ring-2 ring-card" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-sm sm:text-base font-bold tracking-tight leading-tight text-foreground truncate">
|
||||
S3 Drive <span className="bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">Storage</span>
|
||||
</h1>
|
||||
<span className="hidden min-[480px]:inline-flex px-2 py-0.5 rounded-full text-[10px] font-semibold bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20 shrink-0">
|
||||
v1.0 Gateway
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] sm:text-xs text-muted-foreground mt-0.5 truncate max-w-[220px] sm:max-w-none">
|
||||
<span className="hidden sm:inline">Google Drive S3-Compatible Storage Bridge</span>
|
||||
<span className="sm:hidden">Drive S3 Bridge Console</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions & Badges */}
|
||||
<div className="flex items-center gap-2 sm:gap-3 shrink-0">
|
||||
<div className="hidden md:flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary/70 border border-border/60 text-xs text-muted-foreground">
|
||||
<Lock className="h-3.5 w-3.5 text-emerald-500" />
|
||||
<span className="font-medium text-foreground/80">Admin Mode</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSignOut}
|
||||
className="text-xs font-medium gap-1.5 h-8 px-2.5 sm:px-3 rounded-lg hover:border-red-200 hover:bg-red-50/50 hover:text-red-600 dark:hover:bg-red-950/30 dark:hover:border-red-900 transition-colors"
|
||||
>
|
||||
<LogOut className="h-3.5 w-3.5" />
|
||||
<span className="hidden min-[360px]:inline">Sign Out</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="relative z-10 flex-1 max-w-6xl w-full mx-auto p-4 sm:p-6 lg:p-8 space-y-6">
|
||||
{/* Welcome Banner */}
|
||||
<div className="relative overflow-hidden rounded-2xl border border-blue-500/20 bg-gradient-to-br from-card via-card to-blue-500/5 p-6 sm:p-7 shadow-lg shadow-blue-950/5">
|
||||
<div className="absolute top-0 right-0 h-40 w-40 bg-gradient-to-bl from-cyan-500/10 via-blue-500/5 to-transparent rounded-bl-full pointer-events-none" />
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400 text-xs font-semibold">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
<span>Authenticated Session Active</span>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold tracking-tight text-foreground sm:text-3xl">
|
||||
Gateway Overview & Control
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground max-w-2xl font-medium">
|
||||
Connected to Google Drive API with AWS S3 signature verification and multipart upload support via Cloudflare Workers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 self-start sm:self-center">
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-xl bg-card border border-border/80 shadow-xs">
|
||||
<Radio className="h-4 w-4 text-emerald-500 animate-pulse" />
|
||||
<div className="text-left">
|
||||
<div className="text-[10px] uppercase font-bold text-muted-foreground">Edge Network</div>
|
||||
<div className="text-xs font-bold text-foreground">Global CDN Active</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status / Quick Overview cards */}
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
{/* Card 1 */}
|
||||
<Card className="relative overflow-hidden border-border/80 bg-card/90 backdrop-blur-sm shadow-md hover:shadow-lg hover:border-blue-500/30 transition-all group">
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-emerald-500 to-teal-400" />
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Gateway Engine
|
||||
</CardTitle>
|
||||
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-500 group-hover:scale-110 transition-transform">
|
||||
<Server className="h-4 w-4" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold flex items-center gap-2 text-foreground">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-emerald-500 shadow-xs shadow-emerald-500/50" />
|
||||
Online & Healthy
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1.5 flex items-center gap-1">
|
||||
<Activity className="h-3 w-3 text-emerald-500" />
|
||||
<span>Cloudflare Worker edge routing</span>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Card 2 */}
|
||||
<Card className="relative overflow-hidden border-border/80 bg-card/90 backdrop-blur-sm shadow-md hover:shadow-lg hover:border-blue-500/30 transition-all group">
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-blue-500 to-indigo-500" />
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
S3 Storage Backend
|
||||
</CardTitle>
|
||||
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500 group-hover:scale-110 transition-transform">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">Google Drive</div>
|
||||
<p className="text-xs text-muted-foreground mt-1.5 flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3 text-blue-500" />
|
||||
<span>Root directory auto-mapped</span>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Card 3 */}
|
||||
<Card className="relative overflow-hidden border-border/80 bg-card/90 backdrop-blur-sm shadow-md hover:shadow-lg hover:border-cyan-500/30 transition-all group">
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-cyan-500 to-blue-500" />
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Multipart Uploads
|
||||
</CardTitle>
|
||||
<div className="p-2 rounded-lg bg-cyan-500/10 text-cyan-500 group-hover:scale-110 transition-transform">
|
||||
<Layers className="h-4 w-4" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-foreground">Durable Objects</div>
|
||||
<p className="text-xs text-muted-foreground mt-1.5 flex items-center gap-1">
|
||||
<Cpu className="h-3 w-3 text-cyan-500" />
|
||||
<span>Resumable state coordination</span>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Feature Capabilities & Explorer Card */}
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{/* Main Explorer placeholder */}
|
||||
<Card className="lg:col-span-2 border-border/80 bg-card/85 backdrop-blur-sm shadow-md">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-md bg-blue-500/10 text-blue-600 dark:text-blue-400">
|
||||
<FolderTree className="h-4 w-4" />
|
||||
</div>
|
||||
<CardTitle className="text-base font-bold">Bucket Explorer</CardTitle>
|
||||
</div>
|
||||
<span className="text-[11px] font-semibold text-blue-600 dark:text-blue-400 bg-blue-500/10 border border-blue-500/20 px-2 py-0.5 rounded-full">
|
||||
Upcoming Module
|
||||
</span>
|
||||
</div>
|
||||
<CardDescription className="text-xs text-muted-foreground">
|
||||
Full-featured visual browser for files, folders, and storage metadata.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<div className="h-48 rounded-xl border border-dashed border-border flex flex-col items-center justify-center p-6 text-center bg-muted/20">
|
||||
<div className="p-3 rounded-2xl bg-gradient-to-tr from-blue-500/10 to-cyan-500/10 border border-blue-500/20 text-blue-500 mb-3">
|
||||
<UploadCloud className="h-6 w-6 stroke-[1.8]" />
|
||||
</div>
|
||||
<h4 className="text-sm font-semibold text-foreground">Storage Bridge Ready</h4>
|
||||
<p className="text-xs text-muted-foreground mt-1 max-w-sm">
|
||||
Sign-in verification complete. Ready for bucket exploration, object uploads, downloads, and presigned URL operations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Supported S3 Actions badges */}
|
||||
<div className="flex flex-wrap items-center gap-2 pt-2">
|
||||
<span className="text-[11px] font-medium text-muted-foreground mr-1">Supported APIs:</span>
|
||||
{["s3:ListObjectsV2", "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:CreateMultipartUpload"].map((api) => (
|
||||
<span
|
||||
key={api}
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-[11px] font-mono font-medium bg-secondary/80 text-secondary-foreground border border-border/60"
|
||||
>
|
||||
<Sparkles className="h-2.5 w-2.5 text-blue-500" />
|
||||
{api}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Specifications Info card */}
|
||||
<Card className="border-border/80 bg-card/85 backdrop-blur-sm shadow-md flex flex-col justify-between">
|
||||
<div>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-md bg-indigo-500/10 text-indigo-600 dark:text-indigo-400">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
</div>
|
||||
<CardTitle className="text-base font-bold">Security & Protocol</CardTitle>
|
||||
</div>
|
||||
<CardDescription className="text-xs">
|
||||
Compliance and encryption standards
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="p-3 rounded-xl bg-secondary/40 border border-border/60 space-y-1">
|
||||
<div className="text-xs font-semibold text-foreground flex items-center justify-between">
|
||||
<span>AWS Signature Version 4</span>
|
||||
<span className="text-[10px] text-emerald-600 font-bold bg-emerald-500/10 px-1.5 py-0.5 rounded">Enabled</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Cryptographic HMAC-SHA256 request authentication
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 rounded-xl bg-secondary/40 border border-border/60 space-y-1">
|
||||
<div className="text-xs font-semibold text-foreground flex items-center justify-between">
|
||||
<span>Google OAuth 2.0</span>
|
||||
<span className="text-[10px] text-blue-600 font-bold bg-blue-500/10 px-1.5 py-0.5 rounded">Auto-Refresh</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Bearer token rotation via Cloudflare KV store
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-border/60 bg-muted/10">
|
||||
<a
|
||||
href="https://developers.google.com/drive"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center justify-between w-full text-xs font-semibold text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
<span>Google Drive API Reference</span>
|
||||
<ArrowUpRight className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import React, { useState } from "react"
|
||||
import { useNavigate, Navigate } from "react-router"
|
||||
import { Eye, EyeOff, Lock, Database, ArrowRight, Loader2, AlertCircle, Sparkles, Shield, Cloud } from "lucide-react"
|
||||
import { useAuth } from "@/hooks/useAuth"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
|
||||
export default function SignIn() {
|
||||
const [password, setPassword] = useState("")
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const { signIn, isAuthenticated, isLoading } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// If already authenticated and not in loading state, redirect to dashboard
|
||||
if (!isLoading && isAuthenticated) {
|
||||
return <Navigate to="/" replace />
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!password.trim()) {
|
||||
setError("Please enter your password")
|
||||
return
|
||||
}
|
||||
|
||||
setError(null)
|
||||
setSubmitting(true)
|
||||
|
||||
try {
|
||||
const result = await signIn(password)
|
||||
if (result.success) {
|
||||
navigate("/", { replace: true })
|
||||
} else {
|
||||
setError(result.error || "Authentication failed")
|
||||
}
|
||||
} catch {
|
||||
setError("An unexpected error occurred. Please try again.")
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col items-center justify-center bg-background bg-grid-pattern p-4 sm:p-6 lg:p-8 overflow-hidden">
|
||||
{/* Background ambient colorful glow effects */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-[20%] -left-[10%] h-[550px] w-[550px] rounded-full bg-blue-500/15 blur-[130px]" />
|
||||
<div className="absolute top-[30%] -right-[15%] h-[500px] w-[500px] rounded-full bg-cyan-400/15 blur-[120px]" />
|
||||
<div className="absolute -bottom-[20%] left-[25%] h-[450px] w-[450px] rounded-full bg-indigo-500/12 blur-[140px]" />
|
||||
</div>
|
||||
|
||||
<div className="relative w-full max-w-md">
|
||||
{/* Logo and Brand Header */}
|
||||
<div className="mb-8 text-center flex flex-col items-center">
|
||||
<div className="relative mb-4 group cursor-default">
|
||||
<div className="absolute -inset-1 rounded-2xl bg-gradient-to-r from-blue-600 via-indigo-500 to-cyan-400 opacity-70 blur-sm group-hover:opacity-100 transition duration-300" />
|
||||
<div className="relative inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-tr from-blue-600 via-indigo-600 to-cyan-500 text-white shadow-xl shadow-blue-500/25">
|
||||
<Database className="h-8 w-8 text-white stroke-[2.2]" />
|
||||
</div>
|
||||
<div className="absolute -top-1.5 -right-1.5 flex h-6 w-6 items-center justify-center rounded-full bg-emerald-500 text-white ring-2 ring-background shadow-xs">
|
||||
<Cloud className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-600 dark:text-blue-400 text-xs font-semibold mb-3">
|
||||
<Sparkles className="h-3.5 w-3.5 text-blue-500" />
|
||||
<span>Cloudflare Worker & Drive Bridge</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-extrabold tracking-tight text-foreground sm:text-4xl">
|
||||
S3 Drive <span className="bg-gradient-to-r from-blue-600 via-indigo-600 to-cyan-500 bg-clip-text text-transparent">Storage</span>
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground font-medium">
|
||||
High-performance management console for Google Drive S3 gateway
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Sign In Card */}
|
||||
<Card className="border-border/70 bg-card/85 backdrop-blur-xl shadow-2xl shadow-blue-950/5 ring-1 ring-black/5 dark:ring-white/10 rounded-2xl overflow-hidden">
|
||||
<div className="h-1.5 w-full bg-gradient-to-r from-blue-500 via-indigo-500 to-cyan-400" />
|
||||
|
||||
<CardHeader className="space-y-1.5 pt-6 pb-4">
|
||||
<CardTitle className="text-xl font-bold text-center tracking-tight">
|
||||
Sign In to Console
|
||||
</CardTitle>
|
||||
<CardDescription className="text-center text-xs text-muted-foreground">
|
||||
Enter your gateway access password to unlock management tools
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent className="space-y-4 px-6">
|
||||
{/* Error Alert */}
|
||||
{error && (
|
||||
<div
|
||||
className="flex items-start gap-2.5 rounded-xl border border-destructive/20 bg-destructive/10 p-3.5 text-xs text-destructive animate-in fade-in slide-in-from-top-1 duration-200"
|
||||
role="alert"
|
||||
>
|
||||
<AlertCircle className="h-4 w-4 shrink-0 mt-0.5" />
|
||||
<div className="leading-relaxed font-medium">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Password Input Field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-xs font-semibold text-foreground/80">
|
||||
Access Password
|
||||
</Label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3.5 pointer-events-none text-muted-foreground group-focus-within:text-blue-600 transition-colors">
|
||||
<Lock className="h-4 w-4" />
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value)
|
||||
if (error) setError(null)
|
||||
}}
|
||||
placeholder="Enter management password"
|
||||
className="pl-10 pr-10 h-11 text-sm bg-background/80 border-border/80 rounded-xl focus-visible:ring-2 focus-visible:ring-blue-500/40 focus-visible:border-blue-500 transition-all"
|
||||
disabled={submitting}
|
||||
autoComplete="current-password"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3.5 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
||||
tabIndex={-1}
|
||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="pt-3 pb-7 px-6 flex flex-col gap-3">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-11 font-semibold rounded-xl text-sm"
|
||||
disabled={submitting || !password.trim()}
|
||||
>
|
||||
{submitting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
Verifying Credentials...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Unlock Console
|
||||
<ArrowRight className="h-4 w-4 ml-1 transition-transform group-hover:translate-x-1" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
{/* Security badge footer */}
|
||||
<div className="mt-8 flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
||||
<Shield className="h-3.5 w-3.5 text-emerald-500" />
|
||||
<span>Protected by S3 Drive Storage Gateway • SHA-256 Auth</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { BrowserRouter, Routes, Route, Navigate } from "react-router"
|
||||
import SignIn from "@/pages/SignIn"
|
||||
import Dashboard from "@/pages/Dashboard"
|
||||
import { ProtectedRoute } from "@/components/ProtectedRoute"
|
||||
|
||||
export function AppRouter() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/sign-in" element={<SignIn />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user