mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 16:36:34 -05:00
chore(demo): init oidc client demo (#6178)
This commit is contained in:
2
demo/oidc-client/.env.example
Normal file
2
demo/oidc-client/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_OIDC_ISSUER=http://localhost:3000
|
||||
VITE_OIDC_CLIENT_ID=your-client-id
|
||||
13
demo/oidc-client/index.html
Normal file
13
demo/oidc-client/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Better Auth OIDC Client Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
38
demo/oidc-client/package.json
Normal file
38
demo/oidc-client/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@better-auth/oidc-client-demo",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"better-auth": "workspace:*",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"geist": "^1.4.2",
|
||||
"lucide-react": "^0.542.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"oauth4webapi": "^2.10.3",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"wouter": "^3.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react-swc": "^4.0.0",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.2"
|
||||
}
|
||||
}
|
||||
60
demo/oidc-client/src/App.tsx
Normal file
60
demo/oidc-client/src/App.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Toaster } from "sonner";
|
||||
import { Route, Router, Switch } from "wouter";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { AuthProvider } from "@/lib/auth/AuthProvider";
|
||||
import { Dashboard } from "@/pages/Dashboard";
|
||||
import { Home } from "@/pages/Home";
|
||||
|
||||
function App() {
|
||||
const issuer = import.meta.env.VITE_OIDC_ISSUER;
|
||||
const clientId = import.meta.env.VITE_OIDC_CLIENT_ID;
|
||||
|
||||
if (!issuer || !clientId) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4">
|
||||
<div className="text-center space-y-4">
|
||||
<h1 className="text-2xl font-bold text-destructive">
|
||||
Configuration Error
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Please set VITE_OIDC_ISSUER and VITE_OIDC_CLIENT_ID environment
|
||||
variables.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Copy .env.example to .env and configure your OIDC provider settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="dark">
|
||||
<AuthProvider issuer={issuer} clientId={clientId}>
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
{/* Header */}
|
||||
<header className="border-b">
|
||||
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<Logo />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/dashboard" component={Dashboard} />
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
<Toaster richColors closeButton />
|
||||
</div>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
22
demo/oidc-client/src/components/logo.tsx
Normal file
22
demo/oidc-client/src/components/logo.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export const Logo = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
width="60"
|
||||
height="45"
|
||||
viewBox="0 0 60 45"
|
||||
fill="none"
|
||||
className="w-5 h-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0 0H15V15H30V30H15V45H0V30V15V0ZM45 30V15H30V0H45H60V15V30V45H45H30V30H45Z"
|
||||
className="fill-black dark:fill-white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
8
demo/oidc-client/src/components/theme-provider.tsx
Normal file
8
demo/oidc-client/src/components/theme-provider.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
19
demo/oidc-client/src/components/theme-toggle.tsx
Normal file
19
demo/oidc-client/src/components/theme-toggle.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
>
|
||||
<Sun className="h-[1.5rem] w-[1.3rem] dark:hidden" />
|
||||
<Moon className="hidden h-5 w-5 dark:block" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
48
demo/oidc-client/src/components/ui/avatar.tsx
Normal file
48
demo/oidc-client/src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
57
demo/oidc-client/src/components/ui/button.tsx
Normal file
57
demo/oidc-client/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
86
demo/oidc-client/src/components/ui/card.tsx
Normal file
86
demo/oidc-client/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
97
demo/oidc-client/src/index.css
Normal file
97
demo/oidc-client/src/index.css
Normal file
@@ -0,0 +1,97 @@
|
||||
@import "tailwindcss";
|
||||
@config "../tailwind.config.ts";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: hsl(0 0% 100%);
|
||||
--foreground: hsl(20 14.3% 4.1%);
|
||||
--card: hsl(0 0% 100%);
|
||||
--card-foreground: hsl(20 14.3% 4.1%);
|
||||
--popover: hsl(0 0% 100%);
|
||||
--popover-foreground: hsl(20 14.3% 4.1%);
|
||||
--primary: hsl(24 9.8% 10%);
|
||||
--primary-foreground: hsl(60 9.1% 97.8%);
|
||||
--secondary: hsl(60 4.8% 95.9%);
|
||||
--secondary-foreground: hsl(24 9.8% 10%);
|
||||
--muted: hsl(60 4.8% 95.9%);
|
||||
--muted-foreground: hsl(25 5.3% 44.7%);
|
||||
--accent: hsl(60 4.8% 95.9%);
|
||||
--accent-foreground: hsl(24 9.8% 10%);
|
||||
--destructive: hsl(0 84.2% 60.2%);
|
||||
--destructive-foreground: hsl(60 9.1% 97.8%);
|
||||
--border: hsl(20 5.9% 90%);
|
||||
--input: hsl(20 5.9% 90%);
|
||||
--ring: hsl(20 14.3% 4.1%);
|
||||
--radius: 0rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: hsl(20 14.3% 4.1%);
|
||||
--foreground: hsl(60 9.1% 97.8%);
|
||||
--card: hsl(20 14.3% 4.1%);
|
||||
--card-foreground: hsl(60 9.1% 97.8%);
|
||||
--popover: hsl(20 14.3% 4.1%);
|
||||
--popover-foreground: hsl(60 9.1% 97.8%);
|
||||
--primary: hsl(60 9.1% 97.8%);
|
||||
--primary-foreground: hsl(24 9.8% 10%);
|
||||
--secondary: hsl(12 6.5% 15.1%);
|
||||
--secondary-foreground: hsl(60 9.1% 97.8%);
|
||||
--muted: hsl(12 6.5% 15.1%);
|
||||
--muted-foreground: hsl(24 5.4% 63.9%);
|
||||
--accent: hsl(12 6.5% 15.1%);
|
||||
--accent-foreground: hsl(60 9.1% 97.8%);
|
||||
--destructive: hsl(0 62.8% 30.6%);
|
||||
--destructive-foreground: hsl(60 9.1% 97.8%);
|
||||
--border: hsl(12 6.5% 15.1%);
|
||||
--input: hsl(12 6.5% 15.1%);
|
||||
--ring: hsl(24 5.7% 82.9%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family:
|
||||
"Geist Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.no-visible-scrollbar {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.no-visible-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
117
demo/oidc-client/src/lib/auth/AuthProvider.tsx
Normal file
117
demo/oidc-client/src/lib/auth/AuthProvider.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { Client } from "oauth4webapi";
|
||||
import { discoveryRequest, processDiscoveryResponse } from "oauth4webapi";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { AuthContextType } from "./context";
|
||||
import { AuthContext } from "./context";
|
||||
|
||||
type AuthProviderProps = {
|
||||
issuer: string;
|
||||
clientId: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "oidc:state";
|
||||
|
||||
export const AuthProvider: React.FC<AuthProviderProps> = ({
|
||||
children,
|
||||
issuer,
|
||||
clientId,
|
||||
}) => {
|
||||
const client: Client = {
|
||||
client_id: clientId,
|
||||
token_endpoint_auth_method: "none",
|
||||
redirect_uris: [window.location.origin],
|
||||
};
|
||||
|
||||
const [as, setAs] = useState<AuthContextType["as"]>();
|
||||
const [accessToken, setAccessTokenState] =
|
||||
useState<AuthContextType["accessToken"]>();
|
||||
const [idToken, setIdTokenState] = useState<AuthContextType["idToken"]>();
|
||||
const [user, setUserState] = useState<AuthContextType["user"]>();
|
||||
|
||||
// Wrapper functions to persist to localStorage
|
||||
const setAccessToken = (token?: string) => {
|
||||
setAccessTokenState(token);
|
||||
if (token) {
|
||||
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({ ...stored, accessToken: token }),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const setIdToken = (token?: string) => {
|
||||
setIdTokenState(token);
|
||||
if (token) {
|
||||
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({ ...stored, idToken: token }),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const setUser = (userData?: Record<string, unknown>) => {
|
||||
setUserState(userData);
|
||||
if (userData) {
|
||||
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({ ...stored, user: userData }),
|
||||
);
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
// Load from localStorage on mount
|
||||
useEffect(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const { accessToken, idToken, user } = JSON.parse(stored);
|
||||
if (accessToken) setAccessTokenState(accessToken);
|
||||
if (idToken) setIdTokenState(idToken);
|
||||
if (user) setUserState(user);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load auth state from localStorage", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!issuer || as) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const issuerUrl = new URL(issuer);
|
||||
discoveryRequest(issuerUrl, { algorithm: "oidc" })
|
||||
.then((response) => processDiscoveryResponse(issuerUrl, response))
|
||||
.then((as) => setAs(as))
|
||||
.catch((error) =>
|
||||
console.error("Failed to fetch issuer metadata", error),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch issuer metadata", error);
|
||||
}
|
||||
}, [issuer]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
as,
|
||||
client,
|
||||
accessToken,
|
||||
setAccessToken,
|
||||
idToken,
|
||||
setIdToken,
|
||||
user,
|
||||
setUser,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
20
demo/oidc-client/src/lib/auth/context.ts
Normal file
20
demo/oidc-client/src/lib/auth/context.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { AuthorizationServer, Client } from "oauth4webapi";
|
||||
import { createContext } from "react";
|
||||
|
||||
export type AuthContextType = {
|
||||
as?: AuthorizationServer;
|
||||
client: Client;
|
||||
accessToken?: string;
|
||||
setAccessToken: (token?: string) => void;
|
||||
idToken?: string;
|
||||
setIdToken: (token?: string) => void;
|
||||
user?: Record<string, unknown>;
|
||||
setUser: (user?: Record<string, unknown>) => void;
|
||||
};
|
||||
|
||||
export const AuthContext = createContext<AuthContextType>({
|
||||
client: { client_id: "", token_endpoint_auth_method: "none" },
|
||||
setAccessToken: () => {},
|
||||
setIdToken: () => {},
|
||||
setUser: () => {},
|
||||
});
|
||||
230
demo/oidc-client/src/lib/auth/useAuth.ts
Normal file
230
demo/oidc-client/src/lib/auth/useAuth.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
// This code is heavily based on the following example: https://github.com/panva/oauth4webapi/blob/HEAD/examples/oidc.ts
|
||||
|
||||
import * as oauth from "oauth4webapi";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AuthContext } from "./context";
|
||||
|
||||
const webStorageKey = "oidc:auth";
|
||||
|
||||
type LoginParams = {
|
||||
scope?: string;
|
||||
redirectUri?: string;
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const {
|
||||
accessToken,
|
||||
setAccessToken,
|
||||
idToken,
|
||||
setIdToken,
|
||||
setUser,
|
||||
client,
|
||||
user,
|
||||
as,
|
||||
} = useContext(AuthContext);
|
||||
const [isHandlingRedirect, setHandlingRedirect] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const login = async (params?: LoginParams) => {
|
||||
if (!as) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client) {
|
||||
throw new Error("Client is not available");
|
||||
}
|
||||
|
||||
const scope = params?.scope || "openid profile email";
|
||||
let redirectUri = params?.redirectUri;
|
||||
if (
|
||||
!redirectUri &&
|
||||
Array.isArray(client.redirect_uris) &&
|
||||
client.redirect_uris.length > 1
|
||||
) {
|
||||
redirectUri = client.redirect_uris[0]?.toString();
|
||||
}
|
||||
redirectUri = redirectUri || window.location.origin;
|
||||
|
||||
const code_challenge_method = "S256";
|
||||
/**
|
||||
* The following MUST be generated for every redirect to the authorization_endpoint. You must store
|
||||
* the code_verifier and nonce in the end-user session such that it can be recovered as the user
|
||||
* gets redirected from the authorization server back to your application.
|
||||
*/
|
||||
const code_verifier = oauth.generateRandomCodeVerifier();
|
||||
const code_challenge =
|
||||
await oauth.calculatePKCECodeChallenge(code_verifier);
|
||||
let state: string | undefined;
|
||||
let nonce: string | undefined;
|
||||
|
||||
const authorizationUrl = new URL(as.authorization_endpoint!);
|
||||
authorizationUrl.searchParams.set("client_id", client.client_id);
|
||||
authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
||||
authorizationUrl.searchParams.set("response_type", "code");
|
||||
authorizationUrl.searchParams.set("scope", scope);
|
||||
authorizationUrl.searchParams.set("code_challenge", code_challenge);
|
||||
authorizationUrl.searchParams.set(
|
||||
"code_challenge_method",
|
||||
code_challenge_method,
|
||||
);
|
||||
|
||||
state = oauth.generateRandomState();
|
||||
authorizationUrl.searchParams.set("state", state);
|
||||
|
||||
nonce = oauth.generateRandomNonce();
|
||||
authorizationUrl.searchParams.set("nonce", nonce);
|
||||
|
||||
console.log("store code_verifier and nonce in the end-user session");
|
||||
sessionStorage.setItem(
|
||||
webStorageKey,
|
||||
JSON.stringify({ code_verifier, state, nonce, redirectUri }),
|
||||
);
|
||||
|
||||
console.log(
|
||||
"Redirect to Authorization Server",
|
||||
authorizationUrl.toString(),
|
||||
);
|
||||
window.location.assign(authorizationUrl.toString());
|
||||
};
|
||||
|
||||
const handleLoginRedirect = async () => {
|
||||
if (!as || !client || isHandlingRedirect) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHandlingRedirect(true);
|
||||
|
||||
const storage = sessionStorage.getItem(webStorageKey);
|
||||
if (!storage) {
|
||||
console.error("No stored code_verifier and nonce found");
|
||||
return;
|
||||
}
|
||||
sessionStorage.removeItem(webStorageKey);
|
||||
const { code_verifier, state, nonce, redirectUri } = JSON.parse(storage);
|
||||
|
||||
let sub: string;
|
||||
let accessToken: string;
|
||||
|
||||
// @ts-expect-error
|
||||
const currentUrl: URL = new URL(window.location);
|
||||
const params = oauth.validateAuthResponse(as, client, currentUrl, state);
|
||||
if (oauth.isOAuth2Error(params)) {
|
||||
console.error("Error Response", params);
|
||||
setHandlingRedirect(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const authorizationResponse = await oauth.authorizationCodeGrantRequest(
|
||||
as,
|
||||
client,
|
||||
params,
|
||||
redirectUri,
|
||||
code_verifier,
|
||||
);
|
||||
|
||||
let challenges: oauth.WWWAuthenticateChallenge[] | undefined;
|
||||
if (
|
||||
(challenges = oauth.parseWwwAuthenticateChallenges(authorizationResponse))
|
||||
) {
|
||||
for (const challenge of challenges) {
|
||||
console.error("WWW-Authenticate Challenge", challenge);
|
||||
}
|
||||
setHandlingRedirect(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const authorizationCodeResult =
|
||||
await oauth.processAuthorizationCodeOpenIDResponse(
|
||||
as,
|
||||
client,
|
||||
authorizationResponse,
|
||||
nonce,
|
||||
);
|
||||
if (oauth.isOAuth2Error(authorizationCodeResult)) {
|
||||
console.error("Error Response", authorizationCodeResult);
|
||||
setHandlingRedirect(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Access Token Response", authorizationCodeResult);
|
||||
accessToken = authorizationCodeResult.access_token;
|
||||
setAccessToken(accessToken);
|
||||
setIdToken(authorizationCodeResult.id_token);
|
||||
const claims = oauth.getValidatedIdTokenClaims(authorizationCodeResult);
|
||||
console.log("ID Token Claims", claims);
|
||||
sub = claims.sub;
|
||||
|
||||
// UserInfo Request
|
||||
const response = await oauth.userInfoRequest(as, client, accessToken);
|
||||
if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
|
||||
for (const challenge of challenges) {
|
||||
console.error("WWW-Authenticate Challenge", challenge);
|
||||
}
|
||||
setHandlingRedirect(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await oauth.processUserInfoResponse(as, client, sub, response);
|
||||
console.log("UserInfo Response", user);
|
||||
setUser(user);
|
||||
|
||||
setHandlingRedirect(false);
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
redirectUri || window.location.origin,
|
||||
);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
if (!as || !idToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endSessionUrl = new URL(as.end_session_endpoint!);
|
||||
endSessionUrl.searchParams.set(
|
||||
"post_logout_redirect_uri",
|
||||
window.location.origin,
|
||||
);
|
||||
endSessionUrl.searchParams.set("id_token_hint", idToken);
|
||||
console.log("Redirect to End Session Endpoint", endSessionUrl.toString());
|
||||
|
||||
// Clear state and localStorage
|
||||
setAccessToken(undefined);
|
||||
setIdToken(undefined);
|
||||
setUser(undefined);
|
||||
localStorage.removeItem("oidc:state");
|
||||
|
||||
window.location.assign(endSessionUrl.toString());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleAuth = () => {
|
||||
if (window.location.search.includes("code=")) {
|
||||
void handleLoginRedirect()
|
||||
.catch((error) => {
|
||||
console.error("Failed to handle login redirect", error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (as && client) {
|
||||
handleAuth();
|
||||
}
|
||||
}, [window.location.search, as, client]);
|
||||
|
||||
return {
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
isLoading,
|
||||
accessToken,
|
||||
login,
|
||||
handleLoginRedirect,
|
||||
logout,
|
||||
};
|
||||
};
|
||||
7
demo/oidc-client/src/lib/utils.ts
Normal file
7
demo/oidc-client/src/lib/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ClassValue } from "clsx";
|
||||
import { clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
10
demo/oidc-client/src/main.tsx
Normal file
10
demo/oidc-client/src/main.tsx
Normal file
@@ -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>,
|
||||
);
|
||||
163
demo/oidc-client/src/pages/Dashboard.tsx
Normal file
163
demo/oidc-client/src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Key, LogOut, Shield, User } from "lucide-react";
|
||||
import { useLocation } from "wouter";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { useAuth } from "@/lib/auth/useAuth";
|
||||
|
||||
export function Dashboard() {
|
||||
const { user, logout, isAuthenticated, isLoading, accessToken } = useAuth();
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
// Show loading state while checking authentication
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
<div className="text-muted-foreground">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Redirect to home if not authenticated after loading
|
||||
if (!isAuthenticated || !user) {
|
||||
setLocation("/");
|
||||
return null;
|
||||
}
|
||||
|
||||
const getUserInitials = () => {
|
||||
const name = (user.name as string) || (user.email as string) || "U";
|
||||
return name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||
<p className="text-muted-foreground">Welcome back!</p>
|
||||
</div>
|
||||
<Button onClick={() => logout()} variant="outline">
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* User Profile Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="w-5 h-5" />
|
||||
User Profile
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Your account information from the OIDC provider
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-start gap-4">
|
||||
<Avatar className="w-16 h-16">
|
||||
<AvatarImage src={user.picture as string} />
|
||||
<AvatarFallback>{getUserInitials()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 space-y-3">
|
||||
{!!user.name && (
|
||||
<div>
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
Name
|
||||
</div>
|
||||
<div className="text-base">{String(user.name)}</div>
|
||||
</div>
|
||||
)}
|
||||
{!!user.email && (
|
||||
<div>
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
Email
|
||||
</div>
|
||||
<div className="text-base">{String(user.email)}</div>
|
||||
</div>
|
||||
)}
|
||||
{!!user.sub && (
|
||||
<div>
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
Subject (sub)
|
||||
</div>
|
||||
<div className="text-base font-mono text-sm">
|
||||
{String(user.sub)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Session Info Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="w-5 h-5" />
|
||||
Session Information
|
||||
</CardTitle>
|
||||
<CardDescription>Your current OIDC session details</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">
|
||||
Access Token
|
||||
</div>
|
||||
<div className="text-xs font-mono bg-muted p-3 rounded-md break-all">
|
||||
{accessToken
|
||||
? `${accessToken.slice(0, 50)}...`
|
||||
: "Not available"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-muted-foreground mb-1">
|
||||
Status
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
<span className="text-sm">Authenticated</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* All User Claims Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Key className="w-5 h-5" />
|
||||
All Claims
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Complete user information returned by the UserInfo endpoint
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="text-xs font-mono bg-muted p-4 rounded-md overflow-x-auto">
|
||||
{JSON.stringify(user, null, 2)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
125
demo/oidc-client/src/pages/Home.tsx
Normal file
125
demo/oidc-client/src/pages/Home.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useLocation } from "wouter";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAuth } from "@/lib/auth/useAuth";
|
||||
|
||||
const features = [
|
||||
{
|
||||
name: "Authorization Code Flow with PKCE",
|
||||
description: "Secure OAuth 2.0 authorization flow with PKCE extension",
|
||||
},
|
||||
{
|
||||
name: "OpenID Connect",
|
||||
description: "Full OIDC support with ID tokens and user info",
|
||||
},
|
||||
{
|
||||
name: "Session Management",
|
||||
description: "Secure session handling with proper state management",
|
||||
},
|
||||
{
|
||||
name: "Single Sign-On",
|
||||
description: "SSO capabilities with Better Auth OIDC provider",
|
||||
},
|
||||
];
|
||||
|
||||
export function Home() {
|
||||
const { login, isAuthenticated, isLoading } = useAuth();
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-[80vh] flex items-center justify-center">
|
||||
<div className="text-muted-foreground">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-[80vh] flex items-center justify-center overflow-hidden no-visible-scrollbar px-6 md:px-0">
|
||||
<main className="flex flex-col gap-4 items-center justify-center">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="font-bold text-4xl text-black dark:text-white text-center">
|
||||
Better Auth OIDC Client
|
||||
</h3>
|
||||
<p className="text-center break-words text-sm md:text-base text-muted-foreground">
|
||||
Official demo showcasing{" "}
|
||||
<a
|
||||
href="https://better-auth.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="italic underline"
|
||||
>
|
||||
Better Auth
|
||||
</a>{" "}
|
||||
as an OIDC provider with a client application.
|
||||
</p>
|
||||
</div>
|
||||
<div className="md:w-10/12 w-full flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3 pt-2 flex-wrap">
|
||||
<div className="border-y py-2 border-dotted bg-secondary/60 opacity-80">
|
||||
<div className="text-xs flex items-center gap-2 justify-center text-muted-foreground">
|
||||
<span className="text-center">
|
||||
This demo uses Better Auth as an OIDC provider and implements
|
||||
a compliant OIDC client
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
||||
{features.map((feature) => (
|
||||
<div
|
||||
key={feature.name}
|
||||
className="border rounded-md p-4 hover:border-foreground transition-colors"
|
||||
>
|
||||
<div className="font-medium mb-1">{feature.name}</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{feature.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center pt-4">
|
||||
{isAuthenticated ? (
|
||||
<Button
|
||||
onClick={() => setLocation("/dashboard")}
|
||||
size="lg"
|
||||
className="gap-2 min-w-[200px]"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M2 3h20v18H2zm18 16V7H4v12z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Dashboard</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => login()}
|
||||
size="lg"
|
||||
className="gap-2 min-w-[200px]"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 3H3v4h2V5h14v14H5v-2H3v4h18V3zm12 8h-2V9h-2V7h-2v2h2v2H3v2h10v2h-2v2h2v-2h2v-2h2z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Sign In with Better Auth</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
demo/oidc-client/src/vite-env.d.ts
vendored
Normal file
10
demo/oidc-client/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_OIDC_ISSUER: string;
|
||||
readonly VITE_OIDC_CLIENT_ID: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
8
demo/oidc-client/tailwind.config.ts
Normal file
8
demo/oidc-client/tailwind.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config = {
|
||||
darkMode: "class",
|
||||
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||
} satisfies Config;
|
||||
|
||||
export default config;
|
||||
25
demo/oidc-client/tsconfig.json
Normal file
25
demo/oidc-client/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
demo/oidc-client/tsconfig.node.json
Normal file
10
demo/oidc-client/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
14
demo/oidc-client/vite.config.ts
Normal file
14
demo/oidc-client/vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
698
pnpm-lock.yaml
generated
698
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user