diff --git a/.changeset/little-beans-repair.md b/.changeset/little-beans-repair.md new file mode 100644 index 00000000..5c4d7f1b --- /dev/null +++ b/.changeset/little-beans-repair.md @@ -0,0 +1,35 @@ +--- +'@asgardeo/tanstack-router': major +--- + +Add TanStack Router integration package with protected route support + +Introduce `@asgardeo/tanstack-router` - a new integration package that provides seamless authentication support for TanStack Router applications. + +**Key Features:** +- `` component for auth-guarded routes +- Automatic redirect to sign-in for unauthenticated users +- Integration with Asgardeo authentication context +- Type-safe route protection + +**Usage:** + +```tsx +import { ProtectedRoute } from '@asgardeo/tanstack-router'; + +const Route = createFileRoute('/dashboard')({ + component: () => ( + + + + ), +}); +``` + +**Installation:** + +```bash +npm install @asgardeo/tanstack-router +``` + +This is the initial release of the TanStack Router integration package. \ No newline at end of file diff --git a/packages/tanstack-router/CHANGELOG.md b/packages/tanstack-router/CHANGELOG.md new file mode 100644 index 00000000..ca7861b2 --- /dev/null +++ b/packages/tanstack-router/CHANGELOG.md @@ -0,0 +1,7 @@ +# @asgardeo/tanstack-router + +## 0.0.1 + +### Patch Changes + +- Initial implementation of TanStack Router integration for Asgardeo React SDK. This package provides a `ProtectedRoute` component that enables route protection with authentication checks for applications using TanStack Router. The component supports redirects, fallback rendering, and loading states. \ No newline at end of file diff --git a/packages/tanstack-router/LICENSE b/packages/tanstack-router/LICENSE new file mode 100644 index 00000000..f49a4e16 --- /dev/null +++ b/packages/tanstack-router/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/packages/tanstack-router/README.md b/packages/tanstack-router/README.md new file mode 100644 index 00000000..434eb38d --- /dev/null +++ b/packages/tanstack-router/README.md @@ -0,0 +1,283 @@ +# @asgardeo/tanstack-router + +TanStack Router integration for Asgardeo React SDK with protected routes. + +[![npm version](https://img.shields.io/npm/v/@asgardeo/tanstack-router.svg)](https://www.npmjs.com/package/@asgardeo/tanstack-router) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +## Overview + +`@asgardeo/tanstack-router` is a supplementary package that provides seamless integration between Asgardeo authentication and TanStack Router. It offers components to easily protect routes and handle authentication flows in your React applications using TanStack Router. + +## Features + +- 🛡️ **ProtectedRoute Component**: Drop-in component for TanStack Router with built-in authentication +- ⚡ **TypeScript Support**: Full TypeScript support with comprehensive type definitions +- 🎨 **Customizable**: Flexible configuration options for different use cases +- 🔒 **Authentication Guards**: Built-in authentication checking with loading states +- 🚀 **Lightweight**: Minimal bundle size with essential features only +- 🔄 **API Consistency**: Compatible interface with `@asgardeo/react-router` for easy migration + +## Installation + +```bash +npm install @asgardeo/tanstack-router +# or +yarn add @asgardeo/tanstack-router +# or +pnpm add @asgardeo/tanstack-router +``` + +### Peer Dependencies + +This package requires the following peer dependencies: + +```bash +npm install @asgardeo/react @tanstack/react-router react +``` + +## Quick Start + +### 1. Basic Setup with ProtectedRoute + +```tsx +import React from 'react'; +import { createRouter, createRoute, createRootRoute } from '@tanstack/react-router'; +import { AsgardeoProvider } from '@asgardeo/react'; +import { ProtectedRoute } from '@asgardeo/tanstack-router'; +import Dashboard from './components/Dashboard'; +import Profile from './components/Profile'; +import SignIn from './components/SignIn'; + +const rootRoute = createRootRoute({ + component: () =>
Root Layout
, +}); + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () =>
Public Home Page
, +}); + +const signinRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/signin', + component: SignIn, +}); + +const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/dashboard', + component: () => ( + + + + ), +}); + +const profileRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/profile', + component: () => ( + + + + ), +}); + +const routeTree = rootRoute.addChildren([ + indexRoute, + signinRoute, + dashboardRoute, + profileRoute, +]); + +const router = createRouter({ routeTree }); + +function App() { + return ( + + + + ); +} + +export default App; +``` + +### 2. Custom Fallback and Loading States + +```tsx +import { ProtectedRoute } from '@asgardeo/tanstack-router'; + +// Redirect to custom login page +const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/dashboard', + component: () => ( + + + + ), +}); + +// Custom fallback component +const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/dashboard', + component: () => ( + +

Please sign in

+

You need to be signed in to access this page.

+ + }> + +
+ ), +}); + +// Custom loading state +const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/dashboard', + component: () => ( + Loading...} + > + + + ), +}); +``` + +### 3. Integration with Route Layouts + +```tsx +import { ProtectedRoute } from '@asgardeo/tanstack-router'; + +const appLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/app', + component: AppLayout, +}); + +const appDashboardRoute = createRoute({ + getParentRoute: () => appLayoutRoute, + path: '/dashboard', + component: () => ( + + + + ), +}); + +const appProfileRoute = createRoute({ + getParentRoute: () => appLayoutRoute, + path: '/profile', + component: () => ( + + + + ), +}); + +const appSettingsRoute = createRoute({ + getParentRoute: () => appLayoutRoute, + path: '/settings', + component: () => ( + + + + ), +}); +``` + +## API Reference + +### Components + +#### ProtectedRoute + +A component that protects routes based on authentication status. Should be used as the component prop of a TanStack Router route. + +```tsx +interface ProtectedRouteProps { + children: React.ReactElement; + fallback?: React.ReactElement; + redirectTo?: string; + loader?: React.ReactNode; +} +``` + +**Props:** + +- `children` - The component to render when authenticated +- `fallback` - Custom component to render when not authenticated (takes precedence over redirectTo) +- `redirectTo` - URL to redirect to when not authenticated (required unless fallback is provided) +- `loader` - Custom loading component to render while authentication status is being determined + +**Note:** Either `fallback` or `redirectTo` must be provided to handle unauthenticated users. + +## Migration from @asgardeo/react-router + +This package provides the same API as `@asgardeo/react-router`, making migration straightforward: + +```tsx +// Before (React Router) + + +
+ } +/> + +// After (TanStack Router) +const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/dashboard', + component: () => ( + + + + ), +}); +``` + +## Examples + +Check out our sample applications in the repository: + +- [React TanStack Router Sample](../../samples/react-tanstack-router) - Simple React application demonstrating TanStack Router integration with protected routes + +## TypeScript Support + +This package is written in TypeScript and provides comprehensive type definitions. All components are fully typed for the best development experience. + +```tsx +import type { ProtectedRouteProps } from '@asgardeo/tanstack-router'; +``` + +## Contributing + +We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details. + +## License + +This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. + +## Support + +- 📖 [Documentation](https://wso2.com/asgardeo/docs/sdks/react/) +- 💬 [Community Forum](https://stackoverflow.com/questions/tagged/asgardeo) +- 🐛 [Issues](https://github.com/asgardeo/javascript/issues) + +--- + +Built with ❤️ by the [Asgardeo](https://wso2.com/asgardeo/) team. \ No newline at end of file diff --git a/packages/tanstack-router/esbuild.config.mjs b/packages/tanstack-router/esbuild.config.mjs new file mode 100644 index 00000000..efecbbeb --- /dev/null +++ b/packages/tanstack-router/esbuild.config.mjs @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {readFileSync} from 'fs'; +import {build} from 'esbuild'; +import {preserveDirectivesPlugin} from 'esbuild-plugin-preserve-directives'; + +const pkg = JSON.parse(readFileSync('./package.json', 'utf8')); + +const commonOptions = { + bundle: true, + entryPoints: ['src/index.ts'], + external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], + metafile: true, + platform: 'browser', + plugins: [ + preserveDirectivesPlugin({ + directives: ['use client', 'use strict'], + include: /\.(js|ts|jsx|tsx)$/, + exclude: /node_modules/, + }), + ], + target: ['es2020'], +}; + +await build({ + ...commonOptions, + format: 'esm', + outfile: 'dist/index.js', + sourcemap: true, +}); + +await build({ + ...commonOptions, + format: 'cjs', + outfile: 'dist/cjs/index.js', + sourcemap: true, +}); diff --git a/packages/tanstack-router/package.json b/packages/tanstack-router/package.json new file mode 100644 index 00000000..c7cc0f48 --- /dev/null +++ b/packages/tanstack-router/package.json @@ -0,0 +1,71 @@ +{ + "name": "@asgardeo/tanstack-router", + "version": "0.0.0", + "description": "TanStack Router integration for Asgardeo React SDK with protected routes.", + "keywords": [ + "asgardeo", + "react", + "tanstack-router", + "protected-routes", + "authentication" + ], + "homepage": "https://github.com/asgardeo/javascript/tree/main/packages/tanstack-router#readme", + "bugs": { + "url": "https://github.com/asgardeo/javascript/issues" + }, + "author": "WSO2", + "license": "Apache-2.0", + "type": "module", + "main": "dist/cjs/index.js", + "module": "dist/index.js", + "exports": { + "import": "./dist/index.js", + "require": "./dist/cjs/index.js" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "types": "dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/asgardeo/javascript", + "directory": "packages/tanstack-router" + }, + "scripts": { + "build": "pnpm clean && node esbuild.config.mjs && tsc -p tsconfig.lib.json --emitDeclarationOnly --outDir dist", + "clean": "rimraf dist", + "fix:lint": "eslint . --ext .js,.jsx,.ts,.tsx,.cjs,.mjs", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx,.cjs,.mjs", + "test": "vitest", + "test:browser": "vitest --workspace=vitest.workspace.ts", + "typecheck": "tsc -p tsconfig.lib.json" + }, + "devDependencies": { + "@types/node": "^22.15.3", + "@types/react": "^19.1.4", + "@wso2/eslint-plugin": "catalog:", + "@wso2/prettier-config": "catalog:", + "esbuild-plugin-preserve-directives": "^0.0.11", + "esbuild": "^0.25.9", + "eslint": "8.57.0", + "prettier": "^2.6.2", + "react": "^19.1.0", + "@tanstack/react-router": "^1.132.6", + "rimraf": "^6.0.1", + "typescript": "~5.7.2", + "vitest": "^3.1.3" + }, + "peerDependencies": { + "@asgardeo/react": "workspace:^", + "react": ">=16.8.0", + "@tanstack/react-router": ">=1.132.0" + }, + "dependencies": { + "tslib": "^2.8.1" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/tanstack-router/prettier.config.cjs b/packages/tanstack-router/prettier.config.cjs new file mode 100644 index 00000000..0f93eb3d --- /dev/null +++ b/packages/tanstack-router/prettier.config.cjs @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = require('@wso2/prettier-config'); \ No newline at end of file diff --git a/packages/tanstack-router/src/__tests__/index.test.ts b/packages/tanstack-router/src/__tests__/index.test.ts new file mode 100644 index 00000000..fa15644a --- /dev/null +++ b/packages/tanstack-router/src/__tests__/index.test.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { describe, it, expect } from 'vitest'; + +describe('@asgardeo/tanstack-router', () => { + it('should export ProtectedRoute', async () => { + const { ProtectedRoute } = await import('../index'); + expect(ProtectedRoute).toBeDefined(); + }); + + it('should export ProtectedRouteProps interface', async () => { + const exports = await import('../index'); + // Interface check - should not throw + const _: typeof exports.ProtectedRouteProps = undefined as any; + expect(true).toBe(true); + }); + + it('should have the correct named exports', async () => { + const exports = await import('../index'); + const exportNames = Object.keys(exports); + expect(exportNames).toContain('ProtectedRoute'); + }); +}); \ No newline at end of file diff --git a/packages/tanstack-router/src/components/ProtectedRoute.tsx b/packages/tanstack-router/src/components/ProtectedRoute.tsx new file mode 100644 index 00000000..7a58f2bc --- /dev/null +++ b/packages/tanstack-router/src/components/ProtectedRoute.tsx @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FC, ReactElement, ReactNode } from 'react'; +import { Navigate } from '@tanstack/react-router'; +import { useAsgardeo, AsgardeoRuntimeError } from '@asgardeo/react'; + +/** + * Props for the ProtectedRoute component. + */ +export interface ProtectedRouteProps { + /** + * The element to render when the user is authenticated. + */ + children: ReactElement; + /** + * Custom fallback element to render when the user is not authenticated. + * If provided, this takes precedence over redirectTo. + */ + fallback?: ReactElement; + /** + * URL to redirect to when the user is not authenticated. + * Required unless a fallback element is provided. + */ + redirectTo?: string; + /** + * Custom loading element to render while authentication status is being determined. + */ + loader?: ReactNode; +} + +/** + * A protected route component that requires authentication to access. + * + * This component should be used as the component prop of a TanStack Router route. + * It checks authentication status and either renders the protected content, + * shows a loading state, redirects, or shows a fallback. + * + * Either a `redirectTo` prop or a `fallback` prop must be provided to handle + * unauthenticated users. + * + * @example Basic usage with redirect + * ```tsx + * const dashboardRoute = createRouteConfig({ + * path: '/dashboard', + * component: () => ( + * + * + * + * ) + * }) + * ``` + * + * @example With custom fallback + * ```tsx + * const adminRoute = createRouteConfig({ + * path: '/admin', + * component: () => ( + * Access denied}> + * + * + * ) + * }) + * ``` + */ +const ProtectedRoute: FC = ({ children, fallback, redirectTo, loader = null }) => { + const { isSignedIn, isLoading } = useAsgardeo(); + + if (isLoading) { + return loader; + } + + if (isSignedIn) { + return children; + } + + if (fallback) { + return fallback; + } + + if (redirectTo) { + return ; + } + + throw new AsgardeoRuntimeError( + '"fallback" or "redirectTo" prop is required.', + 'ProtectedRoute-ValidationError-001', + 'tanstack-router', + 'Either "fallback" or "redirectTo" prop must be provided to handle unauthenticated users.', + ); +}; + +export default ProtectedRoute; \ No newline at end of file diff --git a/packages/tanstack-router/src/components/__tests__/ProtectedRoute.test.tsx b/packages/tanstack-router/src/components/__tests__/ProtectedRoute.test.tsx new file mode 100644 index 00000000..a0e485c5 --- /dev/null +++ b/packages/tanstack-router/src/components/__tests__/ProtectedRoute.test.tsx @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {describe, it, expect, vi, beforeEach} from 'vitest'; +import {render, screen} from '@testing-library/react'; +import ProtectedRoute from '../ProtectedRoute'; +import {useAsgardeo} from '@asgardeo/react'; + +vi.mock('@asgardeo/react', () => ({ + useAsgardeo: vi.fn(), + AsgardeoRuntimeError: class AsgardeoRuntimeError extends Error { + code: string; + component: string; + traceId: string | undefined; + + constructor(message: string, code: string, component: string, traceId?: string) { + super(message); + this.name = 'AsgardeoRuntimeError'; + this.code = code; + this.component = component; + this.traceId = traceId; + } + }, +})); + +vi.mock('@tanstack/react-router', () => ({ + Navigate: ({to}: {to: string}) =>
Navigate to: {to}
, +})); + +describe('ProtectedRoute', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should render loader when isLoading is true', () => { + vi.mocked(useAsgardeo).mockReturnValue({ + isSignedIn: false, + isLoading: true, + } as any); + + render( + Loading...}> +
Protected Content
+
+ ); + + expect(screen.getByTestId('loader')).toBeDefined(); + expect(screen.queryByTestId('protected-content')).toBeNull(); + }); + + it('should render children when user is authenticated', () => { + vi.mocked(useAsgardeo).mockReturnValue({ + isSignedIn: true, + isLoading: false, + } as any); + + render( + +
Protected Content
+
+ ); + + expect(screen.getByTestId('protected-content')).toBeDefined(); + }); + + it('should render fallback when user is not authenticated and fallback is provided', () => { + vi.mocked(useAsgardeo).mockReturnValue({ + isSignedIn: false, + isLoading: false, + } as any); + + render( + Access Denied} + > +
Protected Content
+
+ ); + + expect(screen.getByTestId('fallback')).toBeDefined(); + expect(screen.queryByTestId('protected-content')).toBeNull(); + }); + + it('should navigate to redirectTo when user is not authenticated and no fallback is provided', () => { + vi.mocked(useAsgardeo).mockReturnValue({ + isSignedIn: false, + isLoading: false, + } as any); + + render( + +
Protected Content
+
+ ); + + const navigate = screen.getByTestId('navigate'); + expect(navigate).toBeDefined(); + expect(navigate.textContent).toBe('Navigate to: /signin'); + expect(screen.queryByTestId('protected-content')).toBeNull(); + }); + + it('should throw error when neither fallback nor redirectTo is provided', () => { + vi.mocked(useAsgardeo).mockReturnValue({ + isSignedIn: false, + isLoading: false, + } as any); + + expect(() => { + render( + +
Protected Content
+
+ ); + }).toThrow('"fallback" or "redirectTo" prop is required.'); + }); + + it('should render null loader by default when isLoading is true and no loader is provided', () => { + vi.mocked(useAsgardeo).mockReturnValue({ + isSignedIn: false, + isLoading: true, + } as any); + + const { container } = render( + +
Protected Content
+
+ ); + + expect(container.textContent).toBe(''); + expect(screen.queryByTestId('protected-content')).toBeNull(); + }); + + it('should prioritize fallback over redirectTo when both are provided', () => { + vi.mocked(useAsgardeo).mockReturnValue({ + isSignedIn: false, + isLoading: false, + } as any); + + render( + Custom Fallback} + > +
Protected Content
+
+ ); + + expect(screen.getByTestId('fallback')).toBeDefined(); + expect(screen.queryByTestId('navigate')).toBeNull(); + expect(screen.queryByTestId('protected-content')).toBeNull(); + }); +}); diff --git a/packages/tanstack-router/src/index.ts b/packages/tanstack-router/src/index.ts new file mode 100644 index 00000000..a9b87e3c --- /dev/null +++ b/packages/tanstack-router/src/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { default as ProtectedRoute } from './components/ProtectedRoute'; +export * from './components/ProtectedRoute'; \ No newline at end of file diff --git a/packages/tanstack-router/tsconfig.eslint.json b/packages/tanstack-router/tsconfig.eslint.json new file mode 100644 index 00000000..36f6e575 --- /dev/null +++ b/packages/tanstack-router/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/.*.js", "**/.*.cjs", "**/.*.ts", "**/*.js", "**/*.cjs", "**/*.ts"] +} \ No newline at end of file diff --git a/packages/tanstack-router/tsconfig.json b/packages/tanstack-router/tsconfig.json new file mode 100644 index 00000000..e927e2f4 --- /dev/null +++ b/packages/tanstack-router/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": ["dom", "dom.iterable", "es6"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/tanstack-router/tsconfig.lib.json b/packages/tanstack-router/tsconfig.lib.json new file mode 100644 index 00000000..cb34fb79 --- /dev/null +++ b/packages/tanstack-router/tsconfig.lib.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "dist", + "declarationDir": "dist", + "types": ["node"], + "skipLibCheck": true, + "declarationMap": true + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "node_modules", + "dist" + ], + "include": ["src/**/*.ts", "src/**/*.tsx"] +} \ No newline at end of file diff --git a/packages/tanstack-router/tsconfig.spec.json b/packages/tanstack-router/tsconfig.spec.json new file mode 100644 index 00000000..9fab35c4 --- /dev/null +++ b/packages/tanstack-router/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] + }, + "include": [ + "vite.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/packages/tanstack-router/vitest.config.ts b/packages/tanstack-router/vitest.config.ts new file mode 100644 index 00000000..1644bfd5 --- /dev/null +++ b/packages/tanstack-router/vitest.config.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {defineConfig} from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + }, +}); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a741c5af..144b2fc8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -476,6 +476,55 @@ importers: specifier: ^3.1.3 version: 3.1.3(@types/node@22.15.30)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@26.1.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + packages/tanstack-router: + dependencies: + '@asgardeo/react': + specifier: workspace:^ + version: link:../react + tslib: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + '@tanstack/react-router': + specifier: ^1.132.6 + version: 1.132.27(react-dom@19.1.1(react@19.1.0))(react@19.1.0) + '@types/node': + specifier: ^22.15.3 + version: 22.15.30 + '@types/react': + specifier: ^19.1.4 + version: 19.1.5 + '@wso2/eslint-plugin': + specifier: 'catalog:' + version: https://gitpkg.now.sh/brionmario/wso2-ui-configs/packages/eslint-plugin?a1fc6eb570653c999828aea9f5027cba06af4391(eslint@8.57.0)(typescript@5.7.3) + '@wso2/prettier-config': + specifier: 'catalog:' + version: https://gitpkg.now.sh/brionmario/wso2-ui-configs/packages/prettier-config?a1fc6eb570653c999828aea9f5027cba06af4391(prettier@2.8.8)(typescript@5.7.3) + esbuild: + specifier: ^0.25.9 + version: 0.25.10 + esbuild-plugin-preserve-directives: + specifier: ^0.0.11 + version: 0.0.11(esbuild@0.25.10) + eslint: + specifier: 8.57.0 + version: 8.57.0 + prettier: + specifier: ^2.6.2 + version: 2.8.8 + react: + specifier: ^19.1.0 + version: 19.1.0 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ~5.7.2 + version: 5.7.3 + vitest: + specifier: ^3.1.3 + version: 3.1.3(@types/node@22.15.30)(@vitest/browser@3.1.3)(jiti@2.6.0)(jsdom@26.1.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + packages/vue: dependencies: '@asgardeo/auth-spa': @@ -585,6 +634,58 @@ importers: specifier: ^2.2.2 version: 2.2.10(typescript@5.1.6) + samples/react-tanstack-router: + dependencies: + '@asgardeo/react': + specifier: workspace:^ + version: link:../../packages/react + '@asgardeo/tanstack-router': + specifier: workspace:^ + version: link:../../packages/tanstack-router + '@tanstack/react-router': + specifier: ^1.132.6 + version: 1.132.27(react-dom@19.1.1(react@19.1.0))(react@19.1.0) + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.1(react@19.1.0) + devDependencies: + '@types/react': + specifier: ^19.1.2 + version: 19.1.5 + '@types/react-dom': + specifier: ^19.1.2 + version: 19.1.5(@types/react@19.1.5) + '@vitejs/plugin-basic-ssl': + specifier: ^2.0.0 + version: 2.0.0(vite@6.3.5(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + '@vitejs/plugin-react': + specifier: ^4.4.1 + version: 4.5.1(vite@6.3.5(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0)) + eslint: + specifier: ^9.25.0 + version: 9.28.0(jiti@2.6.0) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.28.0(jiti@2.6.0)) + eslint-plugin-react-refresh: + specifier: ^0.4.19 + version: 0.4.20(eslint@9.28.0(jiti@2.6.0)) + globals: + specifier: ^16.0.0 + version: 16.1.0 + typescript: + specifier: ~5.8.3 + version: 5.8.3 + typescript-eslint: + specifier: ^8.30.1 + version: 8.33.1(eslint@9.28.0(jiti@2.6.0))(typescript@5.8.3) + vite: + specifier: ^6.3.5 + version: 6.3.5(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + samples/teamspace-react: dependencies: '@asgardeo/i18n': @@ -1257,12 +1358,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.8.0': resolution: {integrity: sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2180,6 +2275,30 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 + '@tanstack/history@1.132.21': + resolution: {integrity: sha512-5ziPz3YarKU5cBJoEJ4muV8cy+5W4oWdJMqW7qosMrK5fb9Qfm+QWX+kO3emKJMu4YOUofVu3toEuuD3x1zXKw==} + engines: {node: '>=12'} + + '@tanstack/react-router@1.132.27': + resolution: {integrity: sha512-fxSm1kxrtl3dQslqqKgYnIbQf7qW4fyWXKQhZYCWBFHO2GT11DWPZTNkTXw5YifOpw2RTmYJBH3C44t1HF95Cw==} + engines: {node: '>=12'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-store@0.7.7': + resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.132.27': + resolution: {integrity: sha512-mNx+nba7mXc7sJdX+kYH4rSW8f7Jx/+0hPOkX4XAnqiq7I1ng3gGqmGuf4+2BYTG2aD+aTSPExUPczy9VNgRfQ==} + engines: {node: '>=12'} + + '@tanstack/store@0.7.7': + resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -2657,11 +2776,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -3062,6 +3176,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@2.0.0: + resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -3663,10 +3780,6 @@ packages: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-scope@8.3.0: - resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3693,10 +3806,6 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@4.2.1: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3717,10 +3826,6 @@ packages: jiti: optional: true - espree@10.3.0: - resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3817,14 +3922,6 @@ packages: fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - fdir@6.4.4: - resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -4368,6 +4465,10 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isbot@5.1.31: + resolution: {integrity: sha512-DPgQshehErHAqSCKDb3rNW03pa2wS/v5evvUqtxt6TTnHRqAG8FdzcSSJs9656pK6Y+NT7K9R4acEYXLHYfpUQ==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -5801,6 +5902,16 @@ packages: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} + seroval-plugins@1.3.3: + resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.3.2: + resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} + engines: {node: '>=10'} + serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} @@ -6142,16 +6253,18 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.13: - resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -6343,6 +6456,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -7247,21 +7365,16 @@ snapshots: '@esbuild/win32-x64@0.25.9': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@8.57.0)': + '@eslint-community/eslint-utils@4.8.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.7.0(eslint@9.28.0(jiti@2.6.0))': + '@eslint-community/eslint-utils@4.8.0(eslint@9.28.0(jiti@2.6.0))': dependencies: eslint: 9.28.0(jiti@2.6.0) eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.8.0(eslint@8.57.0)': - dependencies: - eslint: 8.57.0 - eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} '@eslint/config-array@0.20.0': @@ -7296,7 +7409,7 @@ snapshots: dependencies: ajv: 6.12.6 debug: 4.4.1 - espree: 10.3.0 + espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 @@ -7977,6 +8090,38 @@ snapshots: tailwindcss: 4.1.8 vite: 6.3.5(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) + '@tanstack/history@1.132.21': {} + + '@tanstack/react-router@1.132.27(react-dom@19.1.1(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/history': 1.132.21 + '@tanstack/react-store': 0.7.7(react-dom@19.1.1(react@19.1.0))(react@19.1.0) + '@tanstack/router-core': 1.132.27 + isbot: 5.1.31 + react: 19.1.0 + react-dom: 19.1.1(react@19.1.0) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/react-store@0.7.7(react-dom@19.1.1(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/store': 0.7.7 + react: 19.1.0 + react-dom: 19.1.1(react@19.1.0) + use-sync-external-store: 1.5.0(react@19.1.0) + + '@tanstack/router-core@1.132.27': + dependencies: + '@tanstack/history': 1.132.21 + '@tanstack/store': 0.7.7 + cookie-es: 2.0.0 + seroval: 1.3.2 + seroval-plugins: 1.3.3(seroval@1.3.2) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/store@0.7.7': {} + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 @@ -8357,7 +8502,7 @@ snapshots: '@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.1.6)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.8.0(eslint@8.57.0) '@types/json-schema': 7.0.15 '@types/semver': 7.7.0 '@typescript-eslint/scope-manager': 5.62.0 @@ -8372,7 +8517,7 @@ snapshots: '@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.8.0(eslint@8.57.0) '@types/json-schema': 7.0.15 '@types/semver': 7.7.0 '@typescript-eslint/scope-manager': 5.62.0 @@ -8387,7 +8532,7 @@ snapshots: '@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.1.6)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.8.0(eslint@8.57.0) '@types/json-schema': 7.0.15 '@types/semver': 7.7.0 '@typescript-eslint/scope-manager': 6.21.0 @@ -8401,7 +8546,7 @@ snapshots: '@typescript-eslint/utils@8.33.1(eslint@9.28.0(jiti@2.6.0))(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.6.0)) + '@eslint-community/eslint-utils': 4.8.0(eslint@9.28.0(jiti@2.6.0)) '@typescript-eslint/scope-manager': 8.33.1 '@typescript-eslint/types': 8.33.1 '@typescript-eslint/typescript-estree': 8.33.1(typescript@5.8.3) @@ -8758,16 +8903,10 @@ snapshots: mime-types: 3.0.1 negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.14.1): - dependencies: - acorn: 8.14.1 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 - acorn@8.14.1: {} - acorn@8.15.0: {} agent-base@7.1.3: @@ -9222,6 +9361,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@2.0.0: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -9703,6 +9844,10 @@ snapshots: esbuild: 0.25.9 import-meta-resolve: 3.1.1 + esbuild-plugin-preserve-directives@0.0.11(esbuild@0.25.10): + dependencies: + esbuild: 0.25.10 + esbuild-plugin-preserve-directives@0.0.11(esbuild@0.25.9): dependencies: esbuild: 0.25.9 @@ -10106,11 +10251,6 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-scope@8.3.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -10131,8 +10271,6 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.0: {} - eslint-visitor-keys@4.2.1: {} eslint@8.57.0: @@ -10180,7 +10318,7 @@ snapshots: eslint@9.28.0(jiti@2.6.0): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0(jiti@2.6.0)) + '@eslint-community/eslint-utils': 4.8.0(eslint@9.28.0(jiti@2.6.0)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.20.0 '@eslint/config-helpers': 0.2.2 @@ -10191,16 +10329,16 @@ snapshots: '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.1 escape-string-regexp: 4.0.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -10220,12 +10358,6 @@ snapshots: transitivePeerDependencies: - supports-color - espree@10.3.0: - dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 4.2.0 - espree@10.4.0: dependencies: acorn: 8.15.0 @@ -10339,10 +10471,6 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.4.4(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -10892,6 +11020,8 @@ snapshots: isarray@2.0.5: {} + isbot@5.1.31: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -11308,7 +11438,7 @@ snapshots: mlly@1.7.4: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.6.1 @@ -12408,6 +12538,12 @@ snapshots: transitivePeerDependencies: - supports-color + seroval-plugins@1.3.3(seroval@1.3.2): + dependencies: + seroval: 1.3.2 + + seroval@1.3.2: {} + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 @@ -12865,15 +13001,14 @@ snapshots: text-table@0.2.0: {} + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} - tinyglobby@0.2.13: - dependencies: - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 - tinyglobby@0.2.14: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -13057,6 +13192,10 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + util-deprecate@1.0.2: {} uuid@11.1.0: {} @@ -13118,11 +13257,11 @@ snapshots: vite@6.3.5(@types/node@20.17.50)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.89.0)(terser@5.39.2)(yaml@2.8.0): dependencies: esbuild: 0.25.10 - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.40.2 - tinyglobby: 0.2.13 + rollup: 4.50.0 + tinyglobby: 0.2.14 optionalDependencies: '@types/node': 20.17.50 fsevents: 2.3.3 @@ -13136,11 +13275,11 @@ snapshots: vite@6.3.5(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0): dependencies: esbuild: 0.25.10 - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.40.2 - tinyglobby: 0.2.13 + rollup: 4.50.0 + tinyglobby: 0.2.14 optionalDependencies: '@types/node': 22.15.30 fsevents: 2.3.3 @@ -13154,11 +13293,11 @@ snapshots: vite@6.3.5(@types/node@24.0.3)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0): dependencies: esbuild: 0.25.10 - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.40.2 - tinyglobby: 0.2.13 + rollup: 4.50.0 + tinyglobby: 0.2.14 optionalDependencies: '@types/node': 24.0.3 fsevents: 2.3.3 @@ -13214,7 +13353,7 @@ snapshots: std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinyglobby: 0.2.13 + tinyglobby: 0.2.14 tinypool: 1.0.2 tinyrainbow: 2.0.0 vite: 6.3.5(@types/node@20.17.50)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.89.0)(terser@5.39.2)(yaml@2.8.0) @@ -13254,7 +13393,7 @@ snapshots: std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinyglobby: 0.2.13 + tinyglobby: 0.2.14 tinypool: 1.0.2 tinyrainbow: 2.0.0 vite: 6.3.5(@types/node@22.15.30)(jiti@2.6.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.39.2)(yaml@2.8.0) diff --git a/samples/react-tanstack-router/README.md b/samples/react-tanstack-router/README.md new file mode 100644 index 00000000..a885623d --- /dev/null +++ b/samples/react-tanstack-router/README.md @@ -0,0 +1,149 @@ +# Asgardeo TanStack Router Sample Application + +This is a sample application demonstrating how to integrate [Asgardeo](https://wso2.com/asgardeo/) authentication with [TanStack Router](https://tanstack.com/router) using the `@asgardeo/tanstack-router` package. + +## Features + +- ✅ Authentication with Asgardeo +- ✅ Protected routes using `ProtectedRoute` component +- ✅ TanStack Router integration +- ✅ TypeScript support +- ✅ Vite for fast development + +## Prerequisites + +- Node.js (v18 or higher) +- pnpm +- An Asgardeo account and application + +## Getting Started + +### 1. Set up Asgardeo + +1. Create an account at [Asgardeo](https://wso2.com/asgardeo/) +2. Create a new Single Page Application (SPA) +3. Note down your: + - Organization name + - Client ID +4. Configure the following in your Asgardeo application: + - **Authorized redirect URLs**: `https://localhost:5173` + - **Allowed origins**: `https://localhost:5173` + +### 2. Configure Environment Variables + +Create a `.env.local` file in the root of this sample directory: + +```bash +cp .env.local.example .env.local +``` + +Edit `.env.local` and add your Asgardeo credentials: + +```bash +VITE_ASGARDEO_BASE_URL='https://api.asgardeo.io/t/' +VITE_ASGARDEO_CLIENT_ID='' +``` + +### 3. Install Dependencies + +From the root of the monorepo, run: + +```bash +pnpm install +``` + +### 4. Run the Application + +```bash +cd samples/react-tanstack-router +pnpm dev +``` + +The application will be available at `https://localhost:5173` + +## Project Structure + +``` +src/ +├── pages/ +│ ├── Home.tsx # Unprotected home page +│ └── Dashboard.tsx # Protected dashboard page +├── main.tsx # App entry point with router configuration +├── index.css # Global styles +└── vite-env.d.ts # Vite type definitions +``` + +## Key Implementation Details + +### Protected Routes + +The Dashboard page is protected using the `ProtectedRoute` component from `@asgardeo/tanstack-router`: + +```tsx +import {ProtectedRoute} from '@asgardeo/tanstack-router'; +import Dashboard from './pages/Dashboard'; + +const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/dashboard', + component: () => ( + + + + ), +}); +``` + +### Router Setup + +The application uses TanStack Router's imperative API to create routes: + +```tsx +import {createRouter, createRootRoute, createRoute} from '@tanstack/react-router'; + +const rootRoute = createRootRoute(); +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: Home, +}); + +const routeTree = rootRoute.addChildren([indexRoute, dashboardRoute]); +const router = createRouter({routeTree}); +``` + +### Asgardeo Provider + +The app is wrapped with `AsgardeoProvider` in `main.tsx`: + +```tsx + + + +``` + +## Authentication Flow + +1. User visits the home page (unprotected) +2. User clicks "Sign In" button +3. User is redirected to Asgardeo for authentication +4. After successful authentication, user is redirected back to the application +5. User can now access protected routes like the Dashboard +6. Attempting to access protected routes without authentication redirects to the home page + +## Learn More + +- [Asgardeo Documentation](https://wso2.com/asgardeo/docs/) +- [TanStack Router Documentation](https://tanstack.com/router/latest) +- [@asgardeo/react Documentation](https://github.com/asgardeo/asgardeo-auth-react-sdk) +- [@asgardeo/tanstack-router Package](../../packages/tanstack-router) + +## License + +Apache-2.0 diff --git a/samples/react-tanstack-router/eslint.config.js b/samples/react-tanstack-router/eslint.config.js new file mode 100644 index 00000000..092408a9 --- /dev/null +++ b/samples/react-tanstack-router/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/samples/react-tanstack-router/index.html b/samples/react-tanstack-router/index.html new file mode 100644 index 00000000..06de12e1 --- /dev/null +++ b/samples/react-tanstack-router/index.html @@ -0,0 +1,13 @@ + + + + + + + Asgardeo TanStack Router Sample + + +
+ + + diff --git a/samples/react-tanstack-router/package.json b/samples/react-tanstack-router/package.json new file mode 100644 index 00000000..71a19045 --- /dev/null +++ b/samples/react-tanstack-router/package.json @@ -0,0 +1,32 @@ +{ + "private": true, + "name": "@asgardeo/react-tanstack-router-sample", + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@asgardeo/react": "workspace:^", + "@asgardeo/tanstack-router": "workspace:^", + "@tanstack/react-router": "^1.132.6", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-basic-ssl": "^2.0.0", + "@vitejs/plugin-react": "^4.4.1", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5" + } +} diff --git a/samples/react-tanstack-router/public/vite.svg b/samples/react-tanstack-router/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/samples/react-tanstack-router/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/react-tanstack-router/src/index.css b/samples/react-tanstack-router/src/index.css new file mode 100644 index 00000000..7e877615 --- /dev/null +++ b/samples/react-tanstack-router/src/index.css @@ -0,0 +1,219 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +#root { + width: 100%; + min-height: 100vh; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + +/* Layout Styles */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.nav { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + background-color: #1a1a1a; + margin-bottom: 2rem; +} + +@media (prefers-color-scheme: light) { + .nav { + background-color: #f5f5f5; + border-bottom: 1px solid #e0e0e0; + } +} + +.nav-links { + display: flex; + gap: 1rem; + align-items: center; +} + +.hero { + text-align: center; + padding: 4rem 2rem; +} + +.hero h1 { + margin-bottom: 1rem; +} + +.hero p { + font-size: 1.2em; + margin-bottom: 2rem; + opacity: 0.9; +} + +.page-header { + margin-bottom: 2rem; +} + +.page-header h1 { + margin-bottom: 0.5rem; +} + +.page-header p { + font-size: 1.1em; + opacity: 0.8; +} + +.card { + padding: 2em; + margin: 1rem 0; + border-radius: 8px; + background-color: #1a1a1a; +} + +@media (prefers-color-scheme: light) { + .card { + background-color: #f9f9f9; + border: 1px solid #e0e0e0; + } +} + +.card h2 { + margin-bottom: 0.5rem; +} + +.card p { + margin-top: 1rem; +} + +.card code { + background-color: rgba(100, 108, 255, 0.1); + padding: 0.2em 0.4em; + border-radius: 3px; + font-family: 'Courier New', Courier, monospace; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-top: 2rem; + margin-bottom: 3rem; +} + +.stat-card { + padding: 1.5rem; + border-radius: 8px; + background-color: #2a2a2a; + text-align: center; +} + +@media (prefers-color-scheme: light) { + .stat-card { + background-color: #ffffff; + border: 1px solid #e0e0e0; + } +} + +.stat-card h3 { + font-size: 2em; + margin-bottom: 0.5rem; + color: #646cff; +} + +.user-info { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + padding: 1rem; + border-radius: 8px; + background-color: #2a2a2a; + align-items: center; +} + +@media (prefers-color-scheme: light) { + .user-info { + background-color: #ffffff; + border: 1px solid #e0e0e0; + } +} + +.user-info strong { + color: #646cff; +} diff --git a/samples/react-tanstack-router/src/main.tsx b/samples/react-tanstack-router/src/main.tsx new file mode 100644 index 00000000..9edd43c4 --- /dev/null +++ b/samples/react-tanstack-router/src/main.tsx @@ -0,0 +1,49 @@ +import {StrictMode} from 'react'; +import {createRoot} from 'react-dom/client'; +import {RouterProvider, createRouter, createRootRoute, createRoute} from '@tanstack/react-router'; +import {AsgardeoProvider} from '@asgardeo/react'; +import {ProtectedRoute} from '@asgardeo/tanstack-router'; +import './index.css'; +import Home from './pages/Home'; +import Dashboard from './pages/Dashboard'; + +const rootRoute = createRootRoute(); + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: Home, +}); + +const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/dashboard', + component: () => ( + + + + ), +}); + +const routeTree = rootRoute.addChildren([indexRoute, dashboardRoute]); +const router = createRouter({routeTree}); + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router; + } +} + +createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/samples/react-tanstack-router/src/pages/Dashboard.tsx b/samples/react-tanstack-router/src/pages/Dashboard.tsx new file mode 100644 index 00000000..ea0f874e --- /dev/null +++ b/samples/react-tanstack-router/src/pages/Dashboard.tsx @@ -0,0 +1,88 @@ +import {Link} from '@tanstack/react-router'; +import {useAsgardeo, User} from '@asgardeo/react'; + +export default function Dashboard() { + const {signOut} = useAsgardeo(); + + return ( +
+ + +
+
+

Dashboard

+

This is a protected page. You can only see this because you're authenticated!

+
+ +
+

User Information

+ + {user => ( +
+ {user ? ( +
+
+

+ Name:{' '} + {user?.givenName || user?.name?.givenName || user?.given_name}{' '} + {user?.name?.familyName || user?.familyName || user?.family_name || ''} +

+

+ Email: {user?.email || 'N/A'} +

+

+ Username: {user?.username || 'N/A'} +

+
+
+ ) : ( +

Loading user information...

+ )} +
+ )} +
+
+ +
+
+

24

+

Active Projects

+
+
+

152

+

Tasks Completed

+
+
+

8

+

Team Members

+
+
+

98%

+

Success Rate

+
+
+ +
+

About Protected Routes

+

+ This Dashboard page is wrapped with the ProtectedRoute component from{' '} + @asgardeo/tanstack-router. This ensures that only authenticated users can access this page. +

+

+ If an unauthenticated user tries to access this route, they will be redirected to the home page + automatically. +

+
+
+
+ ); +} diff --git a/samples/react-tanstack-router/src/pages/Home.tsx b/samples/react-tanstack-router/src/pages/Home.tsx new file mode 100644 index 00000000..531167ab --- /dev/null +++ b/samples/react-tanstack-router/src/pages/Home.tsx @@ -0,0 +1,83 @@ +import {Link} from '@tanstack/react-router'; +import {useAsgardeo, SignInButton} from '@asgardeo/react'; + +export default function Home() { + const {isSignedIn, signOut} = useAsgardeo(); + + return ( +
+ + +
+

Welcome to Asgardeo TanStack Router Sample

+

+ This is a sample application demonstrating the integration of Asgardeo authentication with TanStack Router. +

+ + {!isSignedIn && ( +
+ + {isLoading => } + +
+ )} + + {isSignedIn && ( +
+ + + +
+ )} +
+ +
+
+
+

🔐

+

Secure Authentication

+

Powered by Asgardeo

+
+
+

🚀

+

Fast Routing

+

Built with TanStack Router

+
+
+

+

Protected Routes

+

Easy route protection

+
+
+ +
+

About This Sample

+

+ This sample demonstrates how to use the @asgardeo/tanstack-router package to protect routes in + your TanStack Router application. The Dashboard page is protected and requires authentication to access. +

+

+ Try signing in and navigating to the Dashboard to see the ProtectedRoute component in action! +

+
+
+
+ ); +} diff --git a/samples/react-tanstack-router/src/vite-env.d.ts b/samples/react-tanstack-router/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/samples/react-tanstack-router/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/samples/react-tanstack-router/tsconfig.app.json b/samples/react-tanstack-router/tsconfig.app.json new file mode 100644 index 00000000..c9ccbd4c --- /dev/null +++ b/samples/react-tanstack-router/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/samples/react-tanstack-router/tsconfig.json b/samples/react-tanstack-router/tsconfig.json new file mode 100644 index 00000000..3fb7c780 --- /dev/null +++ b/samples/react-tanstack-router/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "files": [], + "references": [{"path": "./tsconfig.app.json"}, {"path": "./tsconfig.node.json"}] +} diff --git a/samples/react-tanstack-router/tsconfig.node.json b/samples/react-tanstack-router/tsconfig.node.json new file mode 100644 index 00000000..9728af2d --- /dev/null +++ b/samples/react-tanstack-router/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/samples/react-tanstack-router/vite.config.ts b/samples/react-tanstack-router/vite.config.ts new file mode 100644 index 00000000..82c30cdb --- /dev/null +++ b/samples/react-tanstack-router/vite.config.ts @@ -0,0 +1,8 @@ +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; +import basicSsl from '@vitejs/plugin-basic-ssl'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), basicSsl()], +});