├── .eslintrc.json
├── app
├── globals.css
├── favicon.ico
├── applications
│ ├── layout.tsx
│ ├── tailwind.css
│ └── page.tsx
├── layout.tsx
├── sign-in
│ └── page.tsx
├── tailwind.css
├── api
│ └── sso-apps
│ │ └── route.ts
└── page.tsx
├── postcss.config.js
├── public
├── logos
│ ├── duo-logo.webp
│ ├── ping-logo.png
│ ├── azure-logo.png
│ ├── descope-logo.png
│ ├── google-logo.png
│ └── okta-logo.webp
├── vercel.svg
├── next.svg
└── placeholder-logo.svg
├── utils
├── data.ts
├── tailwind.ts
└── assets.ts
├── .env.local.example
├── lib
└── utils.ts
├── next.config.mjs
├── middleware.ts
├── components.json
├── components
├── layout
│ ├── theme-switcher
│ │ ├── theme-provider.tsx
│ │ └── theme-switcher.tsx
│ ├── providers.tsx
│ ├── header.tsx
│ └── user-nav.tsx
├── ui
│ ├── label.tsx
│ ├── progress.tsx
│ ├── input.tsx
│ ├── popover.tsx
│ ├── avatar.tsx
│ ├── scroll-area.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ ├── command.tsx
│ └── dropdown-menu.tsx
├── framework-rotation.tsx
├── project-not-found.tsx
├── tailwind.css
└── icons.tsx
├── .gitignore
├── tsconfig.json
├── package.json
├── tailwind.config.ts
└── README.md
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #13131e;
3 | color: #ffffff;
4 | }
5 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/logos/duo-logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/duo-logo.webp
--------------------------------------------------------------------------------
/public/logos/ping-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/ping-logo.png
--------------------------------------------------------------------------------
/public/logos/azure-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/azure-logo.png
--------------------------------------------------------------------------------
/public/logos/descope-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/descope-logo.png
--------------------------------------------------------------------------------
/public/logos/google-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/google-logo.png
--------------------------------------------------------------------------------
/public/logos/okta-logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/descope-sample-apps/descope-sso-applications/HEAD/public/logos/okta-logo.webp
--------------------------------------------------------------------------------
/utils/data.ts:
--------------------------------------------------------------------------------
1 | export const frameworks = ["okta", "azure", "ping", "duo", "google"] as const;
2 |
3 | export type Framework = (typeof frameworks)[number];
4 |
--------------------------------------------------------------------------------
/.env.local.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_DESCOPE_PROJECT_ID=""
2 | NEXT_PUBLIC_DESCOPE_FLOW_ID="sign-up-or-in"
3 |
4 | DESCOPE_MANAGEMENT_KEY=""
5 | SIGN_IN_ROUTE="/sign-in"
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/utils/tailwind.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...classes: ClassValue[]) {
5 | return twMerge(clsx(...classes));
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ["localhost", "cdn.builder.io"],
5 | dangerouslyAllowSVG: true,
6 | },
7 | };
8 |
9 | export default nextConfig;
10 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { authMiddleware } from "@descope/nextjs-sdk/server";
2 |
3 | export default authMiddleware({
4 | projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID,
5 | redirectUrl: "/",
6 | publicRoutes: ["/sign-in", "/"],
7 | });
8 |
9 | export const config = {
10 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
11 | };
12 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "gray",
10 | "cssVariables": false
11 | },
12 | "aliases": {
13 | "utils": "@/lib/utils",
14 | "components": "@/components"
15 | }
16 | }
--------------------------------------------------------------------------------
/components/layout/theme-switcher/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider as NextThemesProvider } from "next-themes";
4 | import { type ThemeProviderProps } from "next-themes/dist/types";
5 |
6 | export default function ThemeProvider({
7 | children,
8 | ...props
9 | }: ThemeProviderProps) {
10 | return {children};
11 | }
12 |
--------------------------------------------------------------------------------
/components/layout/providers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import ThemeProvider from "./theme-switcher/theme-provider";
4 | export default function Providers({ children }: { children: React.ReactNode }) {
5 | return (
6 | <>
7 |
8 | {children}
9 |
10 | >
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/utils/assets.ts:
--------------------------------------------------------------------------------
1 | export const assets = {
2 | gradient:
3 | "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F266e05dba3864799b4715cf4bfd8aa2a",
4 | square:
5 | "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F8997f779f33b430bb22ca667d1b73ade",
6 | okta: "/logos/okta-logo.webp",
7 | ping: "/logos/ping-logo.png",
8 | azure: "/logos/azure-logo.png",
9 | duo: "/logos/duo-logo.webp",
10 | google: "/logos/google-logo.png",
11 | };
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/app/applications/layout.tsx:
--------------------------------------------------------------------------------
1 | import Header from "@/components/layout/header";
2 | import type { Metadata } from "next";
3 |
4 | export const metadata: Metadata = {
5 | title: "Applications",
6 | description: "Applications List",
7 | };
8 |
9 | export default function DashboardLayout({
10 | children,
11 | }: {
12 | children: React.ReactNode;
13 | }) {
14 | return (
15 | <>
16 |
17 |
18 | {children}
19 |
20 | >
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import { AuthProvider } from "@descope/nextjs-sdk";
4 | import Providers from "@/components/layout/providers";
5 | import "./globals.css";
6 |
7 | const inter = Inter({ subsets: ["latin"] });
8 |
9 | export const metadata: Metadata = {
10 | title: "Descope SSO Applications",
11 | description: "Tenant-specific easy-to-use SSO application tiles.",
12 | };
13 |
14 | export default function RootLayout({
15 | children,
16 | }: Readonly<{
17 | children: React.ReactNode;
18 | }>) {
19 | return (
20 |
21 |
22 |
23 | {children}
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/layout/header.tsx:
--------------------------------------------------------------------------------
1 | import ThemeSwitcher from "@/components/layout/theme-switcher/theme-switcher";
2 | import { UserNav } from "./user-nav";
3 | import Image from "next/image";
4 |
5 | export default function Header() {
6 | return (
7 |
8 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/components/framework-rotation.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { assets } from "@/utils/assets";
3 | import { Framework, frameworks } from "@/utils/data";
4 | import { cn } from "@/utils/tailwind";
5 |
6 | type AssetKey = keyof typeof assets;
7 |
8 | export const FrameworkRotation = ({
9 | currentFramework,
10 | }: {
11 | currentFramework: Framework;
12 | }) => {
13 | return (
14 |
15 | {frameworks.map((name, index) => (
16 | frameworks.indexOf(currentFramework as Framework)
24 | ? "opacity-0 -translate-y-2"
25 | : "opacity-0 translate-y-2"
26 | )}
27 | alt="Framework logo"
28 | width="80"
29 | height="80"
30 | />
31 | ))}
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/app/sign-in/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Descope } from "@descope/nextjs-sdk";
4 | import Image from "next/image";
5 | import { assets } from "@/utils/assets";
6 |
7 | export default function SignInPage() {
8 | return (
9 |
10 |
15 |
23 |
30 |
31 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "descope-sso-applications",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@descope/nextjs-sdk": "^0.0.5",
13 | "@radix-ui/react-avatar": "^1.0.4",
14 | "@radix-ui/react-dialog": "^1.0.5",
15 | "@radix-ui/react-dropdown-menu": "^2.0.6",
16 | "@radix-ui/react-icons": "^1.3.0",
17 | "@radix-ui/react-label": "^2.0.2",
18 | "@radix-ui/react-popover": "^1.0.7",
19 | "@radix-ui/react-progress": "^1.0.3",
20 | "@radix-ui/react-scroll-area": "^1.0.5",
21 | "@radix-ui/react-slot": "^1.0.2",
22 | "animejs": "^3.2.2",
23 | "class-variance-authority": "^0.7.0",
24 | "clsx": "^2.1.0",
25 | "cmdk": "^1.0.0",
26 | "lucide-react": "^0.344.0",
27 | "next": "^14.1.4",
28 | "next-themes": "^0.3.0",
29 | "react": "^18",
30 | "react-dom": "^18",
31 | "tailwind-merge": "^2.2.1",
32 | "tailwindcss-animate": "^1.0.7"
33 | },
34 | "devDependencies": {
35 | "@types/node": "^20",
36 | "@types/react": "^18",
37 | "@types/react-dom": "^18",
38 | "autoprefixer": "^10.0.1",
39 | "eslint": "^8",
40 | "eslint-config-next": "14.1.2",
41 | "postcss": "^8",
42 | "tailwindcss": "^3.3.0",
43 | "typescript": "^5"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/layout/theme-switcher/theme-switcher.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
3 | import { useTheme } from "next-themes";
4 |
5 | import { Button } from "@/components/ui/button";
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuItem,
10 | DropdownMenuTrigger,
11 | } from "@/components/ui/dropdown-menu";
12 | type CompProps = {};
13 |
14 | export default function ThemeSwitcher({}: CompProps) {
15 | const { setTheme } = useTheme();
16 | return (
17 |
18 |
19 |
24 |
25 |
26 | setTheme("light")}>
27 | Light
28 |
29 | setTheme("dark")}>
30 | Dark
31 |
32 | setTheme("system")}>
33 | System
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/project-not-found.tsx:
--------------------------------------------------------------------------------
1 | import { assets } from "@/utils/assets";
2 | import { cn } from "@/utils/tailwind";
3 |
4 | import "../app/tailwind.css";
5 |
6 | export default function ProjectNotFound() {
7 | return (
8 |
9 |
16 |
21 |
22 |
23 |
24 | Please provide a Project ID as a parameter in the URL.
25 |
26 |
27 | Example URL:
28 | http://localhost:3000?project=PROJECT_ID&flow=sign-up-or-in
29 |
30 |
31 | You can also deploy this app in{" "}
32 |
38 | Vercel
39 |
40 | , and use environment variables instead.
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/app/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | background: #13131e;
7 | color: #ffffff;
8 | }
9 |
10 | @layer base {
11 | :root {
12 | --background: #13131e;
13 | --foreground: 222.2 84% 4.9%;
14 |
15 | --card: 0 0% 100%;
16 | --card-foreground: 222.2 84% 4.9%;
17 |
18 | --popover: 0 0% 100%;
19 | --popover-foreground: 222.2 84% 4.9%;
20 |
21 | --primary: 222.2 47.4% 11.2%;
22 | --primary-foreground: 210 40% 98%;
23 |
24 | --secondary: 210 40% 96.1%;
25 | --secondary-foreground: 222.2 47.4% 11.2%;
26 |
27 | --muted: 210 40% 96.1%;
28 | --muted-foreground: 215.4 16.3% 46.9%;
29 |
30 | --accent: 210 40% 96.1%;
31 | --accent-foreground: 222.2 47.4% 11.2%;
32 |
33 | --destructive: 0 84.2% 60.2%;
34 | --destructive-foreground: 210 40% 98%;
35 |
36 | --border: 214.3 31.8% 91.4%;
37 | --input: 214.3 31.8% 91.4%;
38 | --ring: 222.2 84% 4.9%;
39 |
40 | --radius: 0.5rem;
41 | }
42 |
43 | .dark {
44 | --background: #13131e;
45 | --foreground: #fff;
46 |
47 | --card: 222.2 84% 4.9%;
48 | --card-foreground: 210 40% 98%;
49 |
50 | --popover: 222.2 84% 4.9%;
51 | --popover-foreground: 210 40% 98%;
52 |
53 | --primary: 210 40% 98%;
54 | --primary-foreground: 222.2 47.4% 11.2%;
55 |
56 | --secondary: 217.2 32.6% 17.5%;
57 | --secondary-foreground: 210 40% 98%;
58 |
59 | --muted: 217.2 32.6% 17.5%;
60 | --muted-foreground: 215 20.2% 65.1%;
61 |
62 | --accent: 217.2 32.6% 17.5%;
63 | --accent-foreground: 210 40% 98%;
64 |
65 | --destructive: 0 62.8% 30.6%;
66 | --destructive-foreground: 210 40% 98%;
67 |
68 | --border: 217.2 32.6% 17.5%;
69 | --input: 217.2 32.6% 17.5%;
70 | --ring: 212.7 26.8% 83.9%;
71 | }
72 | }
73 |
74 | @layer base {
75 | * {
76 | @apply border-border;
77 | }
78 | body {
79 | @apply bg-background text-foreground;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/components/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | background: #13131e;
7 | color: #ffffff;
8 | }
9 |
10 | @layer base {
11 | :root {
12 | --background: #13131e;
13 | --foreground: 222.2 84% 4.9%;
14 |
15 | --card: 0 0% 100%;
16 | --card-foreground: 222.2 84% 4.9%;
17 |
18 | --popover: 0 0% 100%;
19 | --popover-foreground: 222.2 84% 4.9%;
20 |
21 | --primary: 222.2 47.4% 11.2%;
22 | --primary-foreground: 210 40% 98%;
23 |
24 | --secondary: 210 40% 96.1%;
25 | --secondary-foreground: 222.2 47.4% 11.2%;
26 |
27 | --muted: 210 40% 96.1%;
28 | --muted-foreground: 215.4 16.3% 46.9%;
29 |
30 | --accent: 210 40% 96.1%;
31 | --accent-foreground: 222.2 47.4% 11.2%;
32 |
33 | --destructive: 0 84.2% 60.2%;
34 | --destructive-foreground: 210 40% 98%;
35 |
36 | --border: 214.3 31.8% 91.4%;
37 | --input: 214.3 31.8% 91.4%;
38 | --ring: 222.2 84% 4.9%;
39 |
40 | --radius: 0.5rem;
41 | }
42 |
43 | .dark {
44 | --background: #13131e;
45 | --foreground: #fff;
46 |
47 | --card: 222.2 84% 4.9%;
48 | --card-foreground: 210 40% 98%;
49 |
50 | --popover: 222.2 84% 4.9%;
51 | --popover-foreground: 210 40% 98%;
52 |
53 | --primary: 210 40% 98%;
54 | --primary-foreground: 222.2 47.4% 11.2%;
55 |
56 | --secondary: 217.2 32.6% 17.5%;
57 | --secondary-foreground: 210 40% 98%;
58 |
59 | --muted: 217.2 32.6% 17.5%;
60 | --muted-foreground: 215 20.2% 65.1%;
61 |
62 | --accent: 217.2 32.6% 17.5%;
63 | --accent-foreground: 210 40% 98%;
64 |
65 | --destructive: 0 62.8% 30.6%;
66 | --destructive-foreground: 210 40% 98%;
67 |
68 | --border: 217.2 32.6% 17.5%;
69 | --input: 217.2 32.6% 17.5%;
70 | --ring: 212.7 26.8% 83.9%;
71 | }
72 | }
73 |
74 | @layer base {
75 | * {
76 | @apply border-border;
77 | }
78 | body {
79 | @apply bg-background text-foreground;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/applications/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 240 10% 3.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 240 10% 3.9%;
15 |
16 | --primary: 240 5.9% 10%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 240 4.8% 95.9%;
20 | --secondary-foreground: 240 5.9% 10%;
21 |
22 | --muted: 240 4.8% 95.9%;
23 | --muted-foreground: 240 3.8% 46.1%;
24 |
25 | --accent: 240 4.8% 95.9%;
26 | --accent-foreground: 240 5.9% 10%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 240 5.9% 90%;
32 | --input: 240 5.9% 90%;
33 | --ring: 240 10% 3.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 240 10% 3.9%;
40 | --foreground: 0 0% 98%;
41 |
42 | --card: 240 10% 3.9%;
43 | --card-foreground: 0 0% 98%;
44 |
45 | --popover: 240 10% 3.9%;
46 | --popover-foreground: 0 0% 98%;
47 |
48 | --primary: 0 0% 98%;
49 | --primary-foreground: 240 5.9% 10%;
50 |
51 | --secondary: 240 3.7% 15.9%;
52 | --secondary-foreground: 0 0% 98%;
53 |
54 | --muted: 240 3.7% 15.9%;
55 | --muted-foreground: 240 5% 64.9%;
56 |
57 | --accent: 240 3.7% 15.9%;
58 | --accent-foreground: 0 0% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 0 0% 98%;
62 |
63 | --border: 240 3.7% 15.9%;
64 | --input: 240 3.7% 15.9%;
65 | --ring: 240 4.9% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground box-border;
75 | }
76 | }
77 |
78 | @layer utilities {
79 | .min-h-screen {
80 | min-height: 100vh; /* Fallback */
81 | min-height: 100dvh;
82 | }
83 | .h-screen {
84 | height: 100vh; /* Fallback */
85 | height: 100dvh;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-gray-950 dark:focus-visible:ring-gray-300",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./pages/**/*.{ts,tsx}",
6 | "./components/**/*.{ts,tsx}",
7 | "./constants/**/*.{ts,tsx}",
8 | "./app/**/*.{ts,tsx}",
9 | "./src/**/*.{ts,tsx}",
10 | ],
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: "2rem",
15 | screens: {
16 | "2xl": "1400px",
17 | },
18 | },
19 | extend: {
20 | colors: {
21 | border: "hsl(var(--border))",
22 | input: "hsl(var(--input))",
23 | ring: "hsl(var(--ring))",
24 | background: "hsl(var(--background))",
25 | foreground: "hsl(var(--foreground))",
26 | primary: {
27 | DEFAULT: "hsl(var(--primary))",
28 | foreground: "hsl(var(--primary-foreground))",
29 | },
30 | secondary: {
31 | DEFAULT: "hsl(var(--secondary))",
32 | foreground: "hsl(var(--secondary-foreground))",
33 | },
34 | destructive: {
35 | DEFAULT: "hsl(var(--destructive))",
36 | foreground: "hsl(var(--destructive-foreground))",
37 | },
38 | muted: {
39 | DEFAULT: "hsl(var(--muted))",
40 | foreground: "hsl(var(--muted-foreground))",
41 | },
42 | accent: {
43 | DEFAULT: "hsl(var(--accent))",
44 | foreground: "hsl(var(--accent-foreground))",
45 | },
46 | popover: {
47 | DEFAULT: "hsl(var(--popover))",
48 | foreground: "hsl(var(--popover-foreground))",
49 | },
50 | card: {
51 | DEFAULT: "hsl(var(--card))",
52 | foreground: "hsl(var(--card-foreground))",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
59 | },
60 | keyframes: {
61 | "accordion-down": {
62 | from: { height: 0 },
63 | to: { height: "var(--radix-accordion-content-height)" },
64 | },
65 | "accordion-up": {
66 | from: { height: "var(--radix-accordion-content-height)" },
67 | to: { height: 0 },
68 | },
69 | },
70 | animation: {
71 | "accordion-down": "accordion-down 0.2s ease-out",
72 | "accordion-up": "accordion-up 0.2s ease-out",
73 | },
74 | },
75 | },
76 | plugins: [require("tailwindcss-animate")],
77 | };
78 |
--------------------------------------------------------------------------------
/components/icons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertTriangle,
3 | ArrowRight,
4 | Check,
5 | ChevronLeft,
6 | ChevronRight,
7 | CircuitBoardIcon,
8 | Command,
9 | CreditCard,
10 | File,
11 | FileText,
12 | HelpCircle,
13 | Image,
14 | Laptop,
15 | LayoutDashboardIcon,
16 | Loader2,
17 | LogIn,
18 | LucideIcon,
19 | LucideProps,
20 | Moon,
21 | MoreVertical,
22 | Pizza,
23 | Plus,
24 | Settings,
25 | SunMedium,
26 | Trash,
27 | User,
28 | User2Icon,
29 | UserX2Icon,
30 | X,
31 | } from "lucide-react";
32 |
33 | export type Icon = LucideIcon;
34 |
35 | export const Icons = {
36 | dashboard: LayoutDashboardIcon,
37 | logo: Command,
38 | login: LogIn,
39 | close: X,
40 | profile: User2Icon,
41 | spinner: Loader2,
42 | kanban: CircuitBoardIcon,
43 | chevronLeft: ChevronLeft,
44 | chevronRight: ChevronRight,
45 | trash: Trash,
46 | employee: UserX2Icon,
47 | post: FileText,
48 | page: File,
49 | media: Image,
50 | settings: Settings,
51 | billing: CreditCard,
52 | ellipsis: MoreVertical,
53 | add: Plus,
54 | warning: AlertTriangle,
55 | user: User,
56 | arrowRight: ArrowRight,
57 | help: HelpCircle,
58 | pizza: Pizza,
59 | sun: SunMedium,
60 | moon: Moon,
61 | laptop: Laptop,
62 | gitHub: ({ ...props }: LucideProps) => (
63 |
78 | ),
79 | check: Check,
80 | };
81 |
--------------------------------------------------------------------------------
/components/layout/user-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuGroup,
8 | DropdownMenuItem,
9 | DropdownMenuLabel,
10 | DropdownMenuSeparator,
11 | DropdownMenuShortcut,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu";
14 | import { useUser, useDescope } from "@descope/nextjs-sdk/client";
15 | import { useRouter } from "next/navigation";
16 | import { useCallback } from "react";
17 |
18 | export function UserNav() {
19 | const { user } = useUser();
20 | const sdk = useDescope();
21 |
22 | const router = useRouter();
23 |
24 | const handleLogout = useCallback(() => {
25 | sdk.logout();
26 | router.push("/");
27 | }, [sdk, router]);
28 |
29 | if (user) {
30 | // TODO: Add a settings page for admins to restrict and allow access to applications
31 | // const isAdmin = user?.customAttributes?.descoper;
32 |
33 | return (
34 |
35 |
36 |
42 |
43 |
44 |
45 |
46 |
{user?.name}
47 |
48 | {user?.email}
49 |
50 |
51 |
52 | {/* {isAdmin && (
53 | {
55 | router.push("/settings");
56 | }}
57 | >
58 | Settings
59 | ⇧⌘S
60 |
61 | )} */}
62 |
63 |
64 |
69 | Deploy to Vercel
70 |
71 | ⌘D
72 |
73 |
74 | Sign Out
75 | ⇧⌘Q
76 |
77 |
78 |
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/public/placeholder-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Descope SSO Applications 🌐
4 |
5 | Descope SSO Applications is designed to simplify the authentication process for employees and other users utilizing [Dynamic Federation](https://www.descope.com/use-cases/identity-federation) and [IdP-initiated SSO](https://www.descope.com/blog/post/idp-vs-sp-sso?_gl=1*m72a5o*_gcl_aw*R0NMLjE3MTAxODM1MjAuQ2p3S0NBancxN3F2QmhCckVpd0ExclU5dzV0dUFuNm95MG9hRWtVOXpMQkdiVU4ySWI0b0dwS2tHT1o5REl2SGRHbGN4ZTZzaXdMNk9Sb0NBV01RQXZEX0J3RQ..*_gcl_au*MTExNTU5OTU2NS4xNzA2MTI5NTQ5). By acting as a centralized SSO provider, it offers seamless, frictionless sign-in capabilities across multiple SAML SSO-based applications.
6 |
7 | ## Features / Use Cases ✨
8 |
9 | 1. **Streamlined Access**: Provide a unified interface for both internal organization and external users to access SAML applications without any friction, leveraging existing login sessions with other SSO providers. 🔑
10 | 2. **Dynamic Federation**: Authentication requests can be re-routed to any of the SAML SSO providers you've previously configured, allowing you to add multiple IdPs to applications that may only support one IdP, with Descope in the middle guiding your users through to the right provider. 🔄
11 | 3. **Works with Other Auth Providers**: With IdP-initiated SSO, you can add Descope as a layer on top of any current implementation you have (with Auth0, Cognito, Firebase, etc.) to provide you and your users with seamless SAML-based SSO to all of your apps, without changing any app-level SAML configurations. 🤝
12 |
13 | ## Getting Started 🚀
14 |
15 | ### Deployment to Vercel 🚀
16 |
17 | This application will need to be deployed to Vercel or some other hosting service, in order to work with your application. Since this application requires a backend with your specific Descope Management Key, it's important that you properly deploy this app with secured environment secrets.
18 |
19 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdescope-sample-apps%2Fdescope-sso-applications&env=NEXT_PUBLIC_DESCOPE_PROJECT_ID,DESCOPE_MANAGEMENT_KEY)
20 |
21 | ### Local Setup Instructions 🛠️
22 |
23 | 1. **Clone the Repository**
24 |
25 | ```bash
26 | git clone https://github.com/descope-sample-apps/descope-sso-applications
27 | cd descope-sso-applications
28 | ```
29 |
30 | 2. **Install Dependencies**
31 |
32 | ```bash
33 | yarn install
34 | ```
35 |
36 | 3. **Configure Environment Variables**
37 |
38 | Create a `.env.local` file at the root of your project and define the following variables:
39 |
40 | ```env
41 | NEXT_PUBLIC_DESCOPE_PROJECT_ID=YOUR_DESCOPE_PROJECT_ID
42 | DESCOPE_MANAGEMENT_KEY=YOUR_DESCOPE_MANAGEMENT_KEY
43 | NEXT_PUBLIC_DESCOPE_FLOW_ID="sign-up-or-in" // Optional, if you want to use a different flow
44 | ```
45 |
46 | 4. **Run the Application Locally**
47 |
48 | ```bash
49 | yarn dev
50 | ```
51 |
52 | Visit `http://localhost:3000` in your browser to view the app. 🌐
53 |
54 | ## Contributing 🤝
55 |
56 | Contributions are welcome! Please feel free to submit a pull request or open an issue if you have feedback, suggestions, or contributions. ❤️
57 |
58 | ## License 📄
59 |
60 | This project is licensed under the MIT License - see the LICENSE file for details. 🔏
61 |
--------------------------------------------------------------------------------
/app/api/sso-apps/route.ts:
--------------------------------------------------------------------------------
1 | import { createSdk, session } from "@descope/nextjs-sdk/server";
2 |
3 | interface UserResponse {
4 | ssoAppIds?: string[];
5 | }
6 |
7 | interface SSOApplication {
8 | appType: string;
9 | description: string;
10 | enabled: boolean;
11 | id: string;
12 | logo: string;
13 | name: string;
14 | samlSettings: {
15 | idpSsoUrl: string;
16 | };
17 | }
18 |
19 | const sdk = createSdk({
20 | projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID,
21 | managementKey: process.env.DESCOPE_MANAGEMENT_KEY,
22 | });
23 |
24 | export async function GET(req: Request) {
25 | const currentSession = session();
26 | if (!currentSession || !currentSession.token.sub) {
27 | return new Response("Unauthorized", { status: 401 });
28 | }
29 |
30 | if (!sdk.management) {
31 | console.error(
32 | "Management SDK is not available, Make sure you have the DESCOPE_MANAGEMENT_KEY environment variable set"
33 | );
34 | return new Response("Internal error", { status: 500 });
35 | }
36 |
37 | const res = await sdk.management.user.loadByUserId(currentSession.token.sub);
38 |
39 | if (!res.ok) {
40 | console.error("Failed to load user", res.error);
41 | return new Response("Not found", { status: 404 });
42 | }
43 |
44 | const userData = res.data as UserResponse;
45 | let ssoAppsPromise;
46 | let baseURL = "api.descope.com"
47 | if (process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID && process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID.length >= 32) {
48 | const localURL = process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID.substring(1, 5)
49 | baseURL = [baseURL.slice(0, 4), localURL, ".", baseURL.slice(4)].join('')
50 | }
51 | if (!userData.ssoAppIds || userData.ssoAppIds.length === 0) {
52 | // Fetch all applications if no specific apps are assigned to the user
53 | const allAppsUrl = `https://${baseURL}/v1/mgmt/sso/idp/apps/load`;
54 | ssoAppsPromise = fetch(allAppsUrl, {
55 | method: "GET",
56 | headers: {
57 | Authorization: `Bearer ${process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID}:${process.env.DESCOPE_MANAGEMENT_KEY}`,
58 | },
59 | }).then((res) => (res.ok ? res.json().then((data) => data.apps) : []));
60 | } else {
61 | // Fetch specific applications assigned to the user
62 | ssoAppsPromise = Promise.all(
63 | userData.ssoAppIds.map(async (appId) => {
64 | const appUrl = `https://${baseURL}/v1/mgmt/sso/idp/app/load?id=${appId}`;
65 | const appRes = await fetch(appUrl, {
66 | method: "GET",
67 | headers: {
68 | Authorization: `Bearer ${process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID}:${process.env.DESCOPE_MANAGEMENT_KEY}`,
69 | },
70 | });
71 | return appRes.ok ? appRes.json() : null;
72 | })
73 | );
74 | }
75 |
76 | const ssoApps = await ssoAppsPromise;
77 |
78 | // Filter for SAML apps and map to required fields
79 | const validSamlApps = (Array.isArray(ssoApps) ? ssoApps : [ssoApps])
80 | .filter((app) => app !== null && app.appType === "saml")
81 | .map((app: SSOApplication) => ({
82 | description: app.description,
83 | enabled: app.enabled,
84 | name: app.name,
85 | id: app.id,
86 | logo: app.logo,
87 | samlSettings: {
88 | // Create the IdP initiated SSO URL since it isn't returned by default by the Descope API
89 | idpSsoUrl: app.samlSettings.idpSsoUrl.replace("/sso", "/initiate"),
90 | },
91 | }));
92 |
93 | return new Response(JSON.stringify(validSamlApps), { status: 200 });
94 | }
95 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
37 | )
38 | }
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ))
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ))
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ))
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ))
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ))
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | )
142 | }
143 | CommandShortcut.displayName = "CommandShortcut"
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | }
156 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { useState, useEffect } from "react";
5 | import { useSession } from "@descope/nextjs-sdk/client";
6 | import Image from "next/image";
7 | import { assets } from "@/utils/assets";
8 | import { type Framework, frameworks } from "@/utils/data";
9 | import { cn } from "@/utils/tailwind";
10 | import { FrameworkRotation } from "@/components/framework-rotation";
11 | import { Button } from "@/components/ui/button";
12 |
13 | import "./tailwind.css";
14 |
15 | export default function Home() {
16 | const [currentFramework, setCurrentFramework] = useState(
17 | frameworks[0]
18 | );
19 |
20 | const [projectId, setProjectId] = useState(null);
21 | const [showBackground, setShowBackground] = useState(false);
22 | const { isAuthenticated } = useSession();
23 |
24 | useEffect(() => {
25 | let currentIndex = 0;
26 | const rotateFrameworks = () => {
27 | setCurrentFramework(frameworks[currentIndex]);
28 | currentIndex = (currentIndex + 1) % frameworks.length;
29 | };
30 | const intervalId = setInterval(rotateFrameworks, 2000);
31 | return () => clearInterval(intervalId);
32 | }, []);
33 |
34 | useEffect(() => {
35 | setShowBackground(true);
36 | }, []);
37 |
38 | return (
39 |
40 |
52 |
60 |
67 |
73 |
74 |
75 |
76 |
77 |
86 | Easy-to-use
87 | {" "}
88 | SSO Apps with{" "}
89 |
90 |
91 |
92 |
93 | Powered by
94 |
103 |
104 | Descope
105 |
106 |
107 |
108 | {!isAuthenticated && (
109 |
110 |
111 |
130 |
131 |
132 | )}
133 | {isAuthenticated && (
134 |
135 |
136 |
155 |
156 |
157 | )}
158 |
159 |
160 |
161 | );
162 | }
163 |
--------------------------------------------------------------------------------
/app/applications/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Card, CardContent } from "@/components/ui/card";
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogDescription,
8 | DialogFooter,
9 | DialogHeader,
10 | DialogClose,
11 | DialogTrigger,
12 | DialogTitle,
13 | } from "@/components/ui/dialog";
14 | import { Button } from "@/components/ui/button";
15 | import { Progress } from "@/components/ui/progress";
16 | import Image from "next/image";
17 | import Link from "next/link";
18 | import React, { useEffect, useState } from "react";
19 | import { useSession, useUser } from "@descope/nextjs-sdk/client";
20 |
21 | import "./tailwind.css";
22 |
23 | interface SSOApplication {
24 | appType: string;
25 | description: string;
26 | enabled: boolean;
27 | id: string;
28 | logo: string;
29 | name: string;
30 | samlSettings: {
31 | idpSsoUrl: string;
32 | };
33 | }
34 |
35 | export default function AppPage() {
36 | const [apps, setApps] = useState([]);
37 | const [isLoading, setIsLoading] = useState(true);
38 | const [progress, setProgress] = React.useState(13);
39 |
40 | const { isAuthenticated } = useSession();
41 | const { user, isUserLoading } = useUser();
42 |
43 | useEffect(() => {
44 | async function fetchUserInfo() {
45 | setIsLoading(true);
46 | try {
47 | const response = await fetch("/api/sso-apps");
48 | const data = await response.json();
49 | setApps(data);
50 | } catch (error) {
51 | console.error("Failed to fetch user info:", error);
52 | } finally {
53 | setIsLoading(false);
54 | }
55 | }
56 |
57 | fetchUserInfo();
58 | }, []);
59 |
60 | useEffect(() => {
61 | const fakeFetch = setTimeout(() => {
62 | setProgress(66);
63 | setTimeout(() => {
64 | setProgress(100);
65 | setIsLoading(false);
66 | }, 1000);
67 | }, 500);
68 |
69 | return () => clearTimeout(fakeFetch);
70 | }, []);
71 |
72 | const isAdmin = user?.customAttributes?.descoper;
73 |
74 | return (
75 | <>
76 | {isLoading && (
77 |
82 | )}
83 | {!isLoading && !isUserLoading && isAuthenticated && (
84 |
85 |
86 | {apps.length > 0 ? (
87 |
88 | {apps.map((app) => (
89 |
93 |
94 |
102 |
103 | {app.logo ? (
104 |
105 |
112 |
113 | ) : (
114 |
115 | {app.name.charAt(0)}
116 |
117 | )}
118 |
119 | {app.name}
120 |
121 |
122 |
123 |
124 |
125 | ))}
126 | {isAdmin && (
127 |
128 |
167 |
168 | )}
169 |
170 | ) : (
171 |
172 |
173 |
174 | You have no apps configured.
175 |
176 |
177 | Please contact your system administrator if you think this
178 | is an error.
179 |
180 |
181 |
182 | )}
183 |
184 |
185 | )}
186 | >
187 | );
188 | }
189 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------