diff --git a/packages/vue/QUICKSTART.md b/packages/vue/QUICKSTART.md new file mode 100644 index 000000000..b400bec82 --- /dev/null +++ b/packages/vue/QUICKSTART.md @@ -0,0 +1,309 @@ +# `@asgardeo/vue` Quickstart + +This guide will help you quickly integrate Asgardeo authentication into your Vue.js application. + +## Prerequisites + +- [Node.js](https://nodejs.org/en/download) (version 16 or later. LTS version recommended) +- An [Asgardeo account](https://wso2.com/asgardeo/docs/get-started/create-asgardeo-account/) +- Basic knowledge of Vue 3 and the Composition API + +## Step 1: Configure an Application in Asgardeo + +1. **Sign in to Asgardeo Console** + - Go to [Asgardeo Console](https://console.asgardeo.io/) + - Sign in with your Asgardeo account + +2. **Create a New Application** + - Click **Applications** in the left sidebar + - Click **+ New Application** + - Choose **Single Page Application (SPA)** + - Enter your application name (e.g., "My Vue App") + +3. **Note Down Your Credentials from the `Quickstart` tab** + - Copy the **Client ID** from the application details + - Note your **Base URL** (ex: `https://api.asgardeo.io/t/`) + +4. **Configure Application Settings from the `Protocol` tab** + - **Authorized redirect URLs**: Add your application URLs + - `https://localhost:5173` + - **Allowed origins**: Add the same URLs as above + - Click **Update** to save the configuration + +## Step 2: Create a Vue Application + +If you don't have a Vue application set up yet, you can create one using `create-vue`: + +```bash +# Using npm +npm create vue@latest vue-sample + +# Using pnpm +pnpm create vue@latest vue-sample + +# Using yarn +yarn create vue vue-sample +``` + +When prompted, enable TypeScript for a better development experience. + +Alternatively, using Vite: + +```bash +# Using npm +npm create vite@latest vue-sample --template vue-ts + +# Using pnpm +pnpm create vite@latest vue-sample --template vue-ts + +# Using yarn +yarn create vite vue-sample --template vue-ts +``` + +Navigate to your project: + +```bash +cd vue-sample +``` + +## Step 3: Install the SDK + +Install the Asgardeo Vue SDK in your project: + +```bash +# Using npm +npm install @asgardeo/vue + +# Using pnpm +pnpm add @asgardeo/vue + +# Using yarn +yarn add @asgardeo/vue +``` + +## Step 4: Configure the Provider + +Register the Asgardeo plugin and wrap your application with the `AsgardeoProvider` in your main entry file (`src/main.ts`): + +```ts +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' +import { AsgardeoPlugin, AsgardeoProvider } from '@asgardeo/vue' + +const app = createApp(App) + +app.use(AsgardeoPlugin, { + baseUrl: '', + clientId: '', +}) + +app.mount('#app') +``` + +Replace: +- `` with the Base URL you noted in Step 1 (e.g., `https://api.asgardeo.io/t/`) +- `` with the Client ID from Step 1 + +Then wrap your app component with the `AsgardeoProvider` in `src/App.vue`: + +```vue + + + +``` + +## Step 5: Add Sign-in & Sign-out to Your App + +Update your `src/App.vue` to include sign-in and sign-out functionality: + +```vue + + + + + +``` + +## Step 6: Display User Information + +You can also display user information by using the `User` component and the `useUser` composable: + +```vue + + + + + +``` + +### Using the User Render Function Pattern + +Alternatively, you can use the `User` component with a render function: + +```vue + + + +``` + +## Step 7: Try Login + +Run your application and test the sign-in functionality. You should see a "Sign In" button when you're not signed in, and clicking it will redirect you to the Asgardeo sign-in page. + +```bash +# Using npm +npm run dev + +# Using pnpm +pnpm dev + +# Using yarn +yarn dev +``` + +Open your browser and navigate to `http://localhost:5173` (or the port shown in your terminal). Click the "Sign In" button to test the authentication flow. + +## Step 8: Handle Callback + +The SDK automatically handles the OAuth callback redirect. Make sure your application loads correctly after returning from Asgardeo. For custom callback handling, you can use the `Callback` component: + +```vue + + + +``` + +## Next Steps + +🎉 **Congratulations!** You've successfully integrated Asgardeo authentication into your Vue app. + +### What to explore next: + +- **[API Documentation](https://wso2.com/asgardeo/docs/sdks/vue/overview)** - Learn about all available composables and components +- **[Composables Guide](https://wso2.com/asgardeo/docs/sdks/vue/composables)** - Master the composable API (`useUser`, `useOrganization`, etc.) +- **[Custom Styling](https://wso2.com/asgardeo/docs/sdks/vue/customization/styling)** - Customize the appearance of authentication components +- **[Protected Routes](https://wso2.com/asgardeo/docs/sdks/vue/protected-routes)** - Implement route-level authentication +- **[Organizations/Workspaces](https://wso2.com/asgardeo/docs/sdks/vue/organizations)** - Implement multi-tenancy features +- **[User Profile Management](https://wso2.com/asgardeo/docs/sdks/vue/user-profile)** - Access and manage user profile data +- **[Social Login](https://wso2.com/asgardeo/docs/sdks/vue/social-login)** - Enable sign-in with Google, GitHub, Microsoft, and Facebook + +## Common Issues + +### Redirect URL Mismatch +- **Problem**: Getting errors about redirect URI not matching +- **Solution**: Ensure your redirect URLs in Asgardeo match your local/production URLs exactly (including protocol and port) + +### CORS Errors +- **Problem**: Getting CORS-related errors in the console +- **Solution**: Make sure to add your domain to the "Allowed Origins" in your Asgardeo application settings + +### Client ID Not Found +- **Problem**: Authentication fails with "Client ID is invalid" +- **Solution**: Double-check that you're using the correct Client ID from your Asgardeo application and that it's properly configured in the plugin options + +### Plugin Not Registered +- **Problem**: Vue warns about plugin not being registered +- **Solution**: Make sure you've called `app.use(AsgardeoPlugin, { ... })` before mounting your app + +### State Not Updating +- **Problem**: User state doesn't update after sign-in +- **Solution**: Ensure you're using the composable (`useUser`) inside a component wrapped with `AsgardeoProvider` + +## More Resources + +- [Asgardeo Documentation](https://wso2.com/asgardeo/docs/) +- [Vue.js Documentation](https://vuejs.org/) +- [SDK Examples](../../samples/) +- [GitHub Repository](https://github.com/asgardeo/asgardeo-auth-vue-sdk) + +## Getting Help + +If you encounter issues: +1. Check the [FAQs](https://wso2.com/asgardeo/docs/faq/) +2. Search [GitHub Issues](https://github.com/asgardeo/asgardeo-auth-vue-sdk/issues) +3. Ask on the [WSO2 Community Forum](https://wso2.com/community/) +4. Contact [Asgardeo Support](https://wso2.com/asgardeo/support/) diff --git a/packages/vue/README.md b/packages/vue/README.md index f25931867..0ca2a7a50 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -8,6 +8,117 @@ License +## Overview + +The Asgardeo Vue SDK provides a streamlined way to integrate secure authentication and user management into your Vue.js applications. Built for Vue 3, it offers a comprehensive set of composables, components, and utilities to handle authentication flows, user profiles, and multi-tenancy features. + +## Key Features + +- **Easy Integration**: Simple setup with the `AsgardeoPlugin` and provider components +- **Composable API**: Vue 3 composables for reactive authentication state management +- **Pre-built Components**: Ready-to-use components for sign-in, sign-up, user profiles, and more +- **Multi-Tenancy Support**: Built-in organization/workspace management capabilities +- **Customizable UI**: Primitive components and styling options for seamless integration +- **International Support**: Multi-language support with easy localization +- **Type-Safe**: Full TypeScript support for better developer experience + +## Quick Start + +Get started with Asgardeo in your Vue application in minutes. Follow our [Vue Quick Start Guide](./QUICKSTART.md) for step-by-step instructions on integrating authentication into your app. + +## Installation + +```bash +# Using npm +npm install @asgardeo/vue + +# Using pnpm +pnpm add @asgardeo/vue + +# Using yarn +yarn add @asgardeo/vue +``` + +## Basic Usage + +```vue + + + +``` + +## API Documentation + +For complete API documentation including all components, composables, and customization options, see the [Vue SDK Documentation](https://wso2.com/asgardeo/docs/sdks/vue/overview). + +## Supported Features + +### Composables +- `useAsgardeo()` - Main SDK client access +- `useUser()` - User profile and authentication state +- `useOrganization()` - Organization/workspace management +- `useI18n()` - Internationalization +- `useTheme()` - Theme customization +- `useBranding()` - Branding customization +- `useFlow()` - Authentication flow control +- `useFlowMeta()` - Flow metadata access + +### Components +- `SignedIn` - Renders children only when user is authenticated +- `SignedOut` - Renders children only when user is not authenticated +- `SignInButton` - Pre-styled sign-in button +- `SignOutButton` - Pre-styled sign-out button +- `SignUpButton` - Pre-styled sign-up button +- `User` - Access user information with render prop pattern +- `UserProfile` - Display user profile information +- `Organization` - Manage organization/workspace selection +- `Callback` - Handle OAuth callback redirect + +### Social Login Adapters +- Google +- Facebook +- GitHub +- Microsoft + +## Examples + +Check out our [example applications](../../samples/) to see the Vue SDK in action: +- [Test Vue SDK](../../samples/test-vue-sdk/) - Basic example application + +## Documentation + +- [Getting Started](https://wso2.com/asgardeo/docs/get-started/) +- [Vue SDK Guide](https://wso2.com/asgardeo/docs/sdks/vue/) +- [Configuration Options](https://wso2.com/asgardeo/docs/sdks/vue/configuration/) +- [Composables & Components](https://wso2.com/asgardeo/docs/sdks/vue/api/) + +## Support + +For support and questions: +- [Asgardeo Documentation](https://wso2.com/asgardeo/docs/) +- [GitHub Issues](https://github.com/asgardeo/asgardeo-auth-vue-sdk/issues) +- [WSO2 Community Forum](https://wso2.com/community/) + +## Contributing + +We welcome contributions! Please see our [Contributing Guidelines](./CONTRIBUTING.md) for more details. + ## License Licenses this source under the Apache License, Version 2.0 [LICENSE](./LICENSE), You may not use this file except in compliance with the License. + + + + + +Write a readme and QUICKSTART md files for vuejs sdk similar to the react sdk properly. But these should be specific to the Vue.js SDK. \ No newline at end of file diff --git a/packages/vue/esbuild.config.mjs b/packages/vue/esbuild.config.mjs new file mode 100644 index 000000000..39b81fb6f --- /dev/null +++ b/packages/vue/esbuild.config.mjs @@ -0,0 +1,45 @@ +/** + * 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'; + +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', + 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/vue/package.json b/packages/vue/package.json index 2acbf734a..a9f918f9b 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,27 +1,7 @@ { "name": "@asgardeo/vue", - "version": "0.0.10", - "description": "Vue SDK for Asgardeo - Authentication and Identity Management", - "main": "dist/cjs/index.js", - "module": "dist/esm/index.js", - "types": "dist/index.d.ts", - "type": "module", - "author": "WSO2", - "license": "Apache-2.0", - "files": [ - "dist", - "LICENSE", - "README.md" - ], - "homepage": "https://github.com/asgardeo/javascript/tree/main/packages/vue#readme", - "bugs": { - "url": "https://github.com/asgardeo/javascript/issues" - }, - "repository": { - "type": "git", - "url": "https://github.com/asgardeo/javascript", - "directory": "packages/vue" - }, + "version": "0.1.0", + "description": "Vue 3 SDK for Asgardeo - Authentication and Identity Management", "keywords": [ "asgardeo", "authentication", @@ -34,62 +14,61 @@ "sso", "identity-management" ], + "homepage": "https://github.com/asgardeo/javascript/tree/main/packages/vue#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/vue" + }, "scripts": { - "build": "rollup -c", - "dev": "rollup -c -w", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts", - "lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", - "typecheck": "vue-tsc --noEmit", - "test": "vitest --config src/vitest.config.ts --environment=jsdom --passWithNoTests" + "build": "pnpm clean && node esbuild.config.mjs && tsc -p tsconfig.lib.json --emitDeclarationOnly --outDir dist", + "clean": "rimraf dist", + "lint": "eslint . --ext .js,.ts,.vue,.cjs,.mjs", + "lint:fix": "eslint . --ext .js,.ts,.vue,.cjs,.mjs --fix", + "test": "vitest --passWithNoTests", + "typecheck": "tsc -p tsconfig.lib.json" }, "devDependencies": { - "vite": "7.1.12", - "@rollup/plugin-commonjs": "25.0.7", - "@rollup/plugin-image": "3.0.3", - "@rollup/plugin-node-resolve": "15.2.3", - "@rollup/plugin-typescript": "11.1.6", - "@types/node": "20.12.7", - "@vitest/coverage-v8": "3.0.8", - "@vitest/web-worker": "3.0.8", - "@vue/eslint-config-prettier": "8.0.0", - "@vue/eslint-config-typescript": "12.0.0", + "@types/node": "22.15.3", "@vue/test-utils": "2.4.6", "@wso2/eslint-plugin": "catalog:", "@wso2/prettier-config": "catalog:", - "@wso2/stylelint-config": "catalog:", + "esbuild": "0.25.9", "eslint": "8.57.0", - "prettier": "3.2.5", - "rollup": "4.32.0", - "rollup-plugin-dts": "6.1.0", - "rollup-plugin-polyfill-node": "0.13.0", - "rollup-plugin-styles": "4.0.0", - "sass": "1.75.0", - "stylelint": "15.1.0", - "tslib": "2.6.2", - "typescript": "5.1.6", - "vitest": "3.0.8", - "vue-tsc": "2.2.2" - }, - "dependencies": { - "@asgardeo/auth-spa": "3.3.2", - "@asgardeo/js": "0.1.3", - "@vitejs/plugin-vue": "5.2.4", - "base64url": "3.0.1", - "buffer": "6.0.3", - "clsx": "2.1.1", - "fast-sha256": "1.3.0", - "jose": "5.3.0", - "randombytes": "2.1.0" + "jsdom": "26.1.0", + "prettier": "2.6.2", + "rimraf": "6.1.0", + "typescript": "5.7.2", + "vitest": "3.1.3", + "vue": "3.5.30" }, "peerDependencies": { - "vue": ">=3.5.13" + "vue": ">=3.5.0" }, - "exports": { - "types": "./dist/index.d.ts", - "import": "./dist/esm/index.js", - "require": "./dist/cjs/index.js" + "dependencies": { + "@asgardeo/browser": "workspace:*", + "@asgardeo/i18n": "workspace:*", + "tslib": "2.8.1" }, "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/packages/vue/rollup.config.cjs b/packages/vue/rollup.config.cjs deleted file mode 100644 index 0598532e4..000000000 --- a/packages/vue/rollup.config.cjs +++ /dev/null @@ -1,72 +0,0 @@ -/** - * 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. - */ - -const commonjs = require('@rollup/plugin-commonjs'); -const image = require('@rollup/plugin-image'); -const nodeResolve = require('@rollup/plugin-node-resolve'); -const typescript = require('@rollup/plugin-typescript'); -const dts = require('rollup-plugin-dts'); -const nodePolyfills = require('rollup-plugin-polyfill-node'); -const styles = require('rollup-plugin-styles'); -const pkg = require('./package.json'); - -module.exports = [ - { - cache: false, - external: ['vue'], - input: 'src/index.ts', - onwarn(warning, warn) { - // Suppress this error message... there are hundreds of them - if (warning.code === 'MODULE_LEVEL_DIRECTIVE') return; - // Use default for everything else - warn(warning); - }, - output: [ - { - file: pkg.main, - format: 'cjs', - sourcemap: true, - }, - { - file: pkg.module, - format: 'esm', - sourcemap: true, - }, - ], - plugins: [ - nodePolyfills(), - nodeResolve({ - browser: true, - preferBuiltins: true, - }), - commonjs(), - typescript({tsconfig: './tsconfig.lib.json'}), - styles({ - mode: 'inject', - }), - image(), - ], - }, - { - cache: false, - external: [/\.(sass|scss|css)$/] /* ignore style files */, - input: 'dist/esm/types/index.d.ts', - output: [{file: 'dist/index.d.ts', format: 'esm'}], - plugins: [dts.default()], - }, -]; diff --git a/packages/vue/src/AsgardeoVueClient.ts b/packages/vue/src/AsgardeoVueClient.ts new file mode 100644 index 000000000..a220219d4 --- /dev/null +++ b/packages/vue/src/AsgardeoVueClient.ts @@ -0,0 +1,514 @@ +/** + * 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 { + AsgardeoBrowserClient, + flattenUserSchema, + generateFlattenedUserProfile, + UserProfile, + SignInOptions, + SignOutOptions, + User, + generateUserProfile, + EmbeddedFlowExecuteResponse, + SignUpOptions, + EmbeddedFlowExecuteRequestPayload, + AsgardeoRuntimeError, + executeEmbeddedSignUpFlow, + EmbeddedSignInFlowHandleRequestPayload, + executeEmbeddedSignInFlow, + executeEmbeddedSignInFlowV2, + Organization, + IdToken, + EmbeddedFlowExecuteRequestConfig, + deriveOrganizationHandleFromBaseUrl, + AllOrganizationsApiResponse, + extractUserClaimsFromIdToken, + TokenResponse, + HttpRequestConfig, + HttpResponse, + navigate, + getRedirectBasedSignUpUrl, + Config, + TokenExchangeRequestConfig, + Platform, + isEmpty, + EmbeddedSignInFlowResponseV2, + executeEmbeddedSignUpFlowV2, + EmbeddedSignInFlowStatusV2, +} from '@asgardeo/browser'; +import AuthAPI from './__temp__/api'; +import getAllOrganizations from './api/getAllOrganizations'; +import getMeOrganizations from './api/getMeOrganizations'; +import getSchemas from './api/getSchemas'; +import getScim2Me from './api/getScim2Me'; +import {AsgardeoVueConfig} from './models/config'; + +/** + * Client for implementing Asgardeo in Vue applications. + * This class provides the core functionality for managing user authentication and sessions. + * + * @typeParam T - Configuration type that extends AsgardeoVueConfig. + */ +class AsgardeoVueClient extends AsgardeoBrowserClient { + private asgardeo: AuthAPI; + + private loadingState: boolean = false; + + private clientInstanceId: number; + + constructor(instanceId: number = 0) { + super(); + this.clientInstanceId = instanceId; + + // FIXME: This has to be the browser client from `@asgardeo/browser` package. + this.asgardeo = new AuthAPI(undefined, instanceId); + } + + public getInstanceId(): number { + return this.clientInstanceId; + } + + private setLoading(loading: boolean): void { + this.loadingState = loading; + } + + private async withLoading(operation: () => Promise): Promise { + this.setLoading(true); + try { + const result: TResult = await operation(); + return result; + } finally { + this.setLoading(false); + } + } + + override initialize(config: AsgardeoVueConfig): Promise { + let resolvedOrganizationHandle: string | undefined = config?.organizationHandle; + + if (!resolvedOrganizationHandle) { + resolvedOrganizationHandle = deriveOrganizationHandleFromBaseUrl(config?.baseUrl); + } + + return this.withLoading(async () => + this.asgardeo.init({...config, organizationHandle: resolvedOrganizationHandle} as any), + ); + } + + override reInitialize(config: Partial): Promise { + return this.withLoading(async () => { + let isInitialized: boolean; + + try { + await this.asgardeo.reInitialize(config); + isInitialized = true; + } catch (error) { + throw new AsgardeoRuntimeError( + `Failed to check if the client is initialized: ${error instanceof Error ? error.message : String(error)}`, + 'AsgardeoVueClient-reInitialize-RuntimeError-001', + 'vue', + 'An error occurred while checking the initialization status of the client.', + ); + } + + return isInitialized; + }); + } + + // eslint-disable-next-line class-methods-use-this + override async updateUserProfile(): Promise { + throw new Error('Not implemented'); + } + + override async getUser(options?: any): Promise { + try { + let baseUrl: string = options?.baseUrl; + + if (!baseUrl) { + const configData: any = await this.asgardeo.getConfigData(); + baseUrl = configData?.baseUrl; + } + + const profile: User = await getScim2Me({baseUrl}); + const schemas: any = await getSchemas({baseUrl}); + + return generateUserProfile(profile, flattenUserSchema(schemas)); + } catch (error) { + return extractUserClaimsFromIdToken(await this.getDecodedIdToken()); + } + } + + async getDecodedIdToken(sessionId?: string): Promise { + return this.asgardeo.getDecodedIdToken(sessionId); + } + + async getIdToken(): Promise { + return this.withLoading(async () => this.asgardeo.getIdToken()); + } + + override async getUserProfile(options?: any): Promise { + return this.withLoading(async () => { + try { + let baseUrl: string = options?.baseUrl; + + if (!baseUrl) { + const configData: any = await this.asgardeo.getConfigData(); + baseUrl = configData?.baseUrl; + } + + const profile: User = await getScim2Me({baseUrl, instanceId: this.getInstanceId()}); + const schemas: any = await getSchemas({baseUrl, instanceId: this.getInstanceId()}); + + const processedSchemas: any = flattenUserSchema(schemas); + + const output: UserProfile = { + flattenedProfile: generateFlattenedUserProfile(profile, processedSchemas), + profile, + schemas: processedSchemas, + }; + + return output; + } catch (error) { + return { + flattenedProfile: extractUserClaimsFromIdToken(await this.getDecodedIdToken()), + profile: extractUserClaimsFromIdToken(await this.getDecodedIdToken()), + schemas: [], + }; + } + }); + } + + override async getMyOrganizations(options?: any): Promise { + try { + let baseUrl: string = options?.baseUrl; + + if (!baseUrl) { + const configData: any = await this.asgardeo.getConfigData(); + baseUrl = configData?.baseUrl; + } + + return await getMeOrganizations({baseUrl, instanceId: this.getInstanceId()}); + } catch (error) { + throw new AsgardeoRuntimeError( + `Failed to fetch the user's associated organizations: ${ + error instanceof Error ? error.message : String(error) + }`, + 'AsgardeoVueClient-getMyOrganizations-RuntimeError-001', + 'vue', + 'An error occurred while fetching associated organizations of the signed-in user.', + ); + } + } + + override async getAllOrganizations(options?: any): Promise { + try { + let baseUrl: string = options?.baseUrl; + + if (!baseUrl) { + const configData: any = await this.asgardeo.getConfigData(); + baseUrl = configData?.baseUrl; + } + + return await getAllOrganizations({baseUrl, instanceId: this.getInstanceId()}); + } catch (error) { + throw new AsgardeoRuntimeError( + `Failed to fetch all organizations: ${error instanceof Error ? error.message : String(error)}`, + 'AsgardeoVueClient-getAllOrganizations-RuntimeError-001', + 'vue', + 'An error occurred while fetching all the organizations associated with the user.', + ); + } + } + + override async getCurrentOrganization(): Promise { + try { + return await this.withLoading(async () => { + const idToken: IdToken = await this.getDecodedIdToken(); + return { + id: idToken?.org_id, + name: idToken?.org_name, + orgHandle: idToken?.org_handle, + }; + }); + } catch (error) { + throw new AsgardeoRuntimeError( + `Failed to fetch the current organization: ${error instanceof Error ? error.message : String(error)}`, + 'AsgardeoVueClient-getCurrentOrganization-RuntimeError-001', + 'vue', + 'An error occurred while fetching the current organization of the signed-in user.', + ); + } + } + + override async switchOrganization(organization: Organization): Promise { + return this.withLoading(async () => { + try { + const configData: any = await this.asgardeo.getConfigData(); + const sourceInstanceId: number | undefined = configData?.organizationChain?.sourceInstanceId; + + if (!organization.id) { + throw new AsgardeoRuntimeError( + 'Organization ID is required for switching organizations', + 'vue-AsgardeoVueClient-SwitchOrganizationError-001', + 'vue', + 'The organization object must contain a valid ID to perform the organization switch.', + ); + } + + const exchangeConfig: TokenExchangeRequestConfig = { + attachToken: false, + data: { + client_id: '{{clientId}}', + grant_type: 'organization_switch', + scope: '{{scopes}}', + switching_organization: organization.id, + token: '{{accessToken}}', + }, + id: 'organization-switch', + returnsSession: true, + signInRequired: sourceInstanceId === undefined, + }; + + return (await this.asgardeo.exchangeToken(exchangeConfig, () => {})) as TokenResponse | Response; + } catch (error) { + throw new AsgardeoRuntimeError( + `Failed to switch organization: ${error.message || error}`, + 'vue-AsgardeoVueClient-SwitchOrganizationError-003', + 'vue', + 'An error occurred while switching to the specified organization. Please try again.', + ); + } + }); + } + + override isLoading(): boolean { + return this.loadingState || this.asgardeo.isLoading(); + } + + async isInitialized(): Promise { + return this.asgardeo.isInitialized(); + } + + override async isSignedIn(): Promise { + return this.asgardeo.isSignedIn(); + } + + override getConfiguration(): T { + return this.asgardeo.getConfigData() as unknown as T; + } + + override async exchangeToken(config: TokenExchangeRequestConfig): Promise { + return this.withLoading( + async () => this.asgardeo.exchangeToken(config, () => {}) as unknown as TokenResponse | Response, + ); + } + + override signIn( + options?: SignInOptions, + sessionId?: string, + onSignInSuccess?: (afterSignInUrl: string) => void, + ): Promise; + override signIn( + payload: EmbeddedSignInFlowHandleRequestPayload, + request: EmbeddedFlowExecuteRequestConfig, + sessionId?: string, + onSignInSuccess?: (afterSignInUrl: string) => void, + ): Promise; + override async signIn(...args: any[]): Promise { + return this.withLoading(async () => { + const arg1: any = args[0]; + const arg2: any = args[1]; + + const config: AsgardeoVueConfig | undefined = (await this.asgardeo.getConfigData()) as + | AsgardeoVueConfig + | undefined; + + const platformFromStorage: string | null = sessionStorage.getItem('asgardeo_platform'); + const isV2Platform: boolean = + (config && config.platform === Platform.AsgardeoV2) || platformFromStorage === 'AsgardeoV2'; + + if (isV2Platform && typeof arg1 === 'object' && arg1 !== null && (arg1 as any).callOnlyOnRedirect === true) { + return undefined as any; + } + + if ( + isV2Platform && + typeof arg1 === 'object' && + arg1 !== null && + !isEmpty(arg1) && + ('flowId' in arg1 || 'applicationId' in arg1) + ) { + const authIdFromUrl: string = new URL(window.location.href).searchParams.get('authId'); + const authIdFromStorage: string = sessionStorage.getItem('asgardeo_auth_id'); + const authId: string = authIdFromUrl || authIdFromStorage; + const baseUrlFromStorage: string = sessionStorage.getItem('asgardeo_base_url'); + const baseUrl: string = config?.baseUrl || baseUrlFromStorage; + + const response: EmbeddedSignInFlowResponseV2 = await executeEmbeddedSignInFlowV2({ + authId, + baseUrl, + payload: arg1 as EmbeddedSignInFlowHandleRequestPayload, + url: arg2?.url, + }); + + if ( + isV2Platform && + response && + typeof response === 'object' && + response['flowStatus'] === EmbeddedSignInFlowStatusV2.Complete && + response['assertion'] + ) { + const decodedAssertion: { + [key: string]: unknown; + exp?: number; + iat?: number; + scope?: string; + } = await this.decodeJwtToken<{ + [key: string]: unknown; + exp?: number; + iat?: number; + scope?: string; + }>(response['assertion']); + + const createdAt: number = decodedAssertion.iat ? decodedAssertion.iat * 1000 : Date.now(); + const expiresIn: number = + decodedAssertion.exp && decodedAssertion.iat ? decodedAssertion.exp - decodedAssertion.iat : 3600; + + await this.setSession({ + access_token: response['assertion'], + created_at: createdAt, + expires_in: expiresIn, + id_token: response['assertion'], + scope: decodedAssertion.scope, + token_type: 'Bearer', + }); + } + + return response; + } + + if (typeof arg1 === 'object' && 'flowId' in arg1 && typeof arg2 === 'object' && 'url' in arg2) { + return executeEmbeddedSignInFlow({ + payload: arg1, + url: arg2.url, + }); + } + + return (await this.asgardeo.signIn(arg1 as any)) as unknown as Promise; + }); + } + + override async signInSilently(options?: SignInOptions): Promise { + return this.asgardeo.signInSilently(options as Record); + } + + override signOut(options?: SignOutOptions, afterSignOut?: (afterSignOutUrl: string) => void): Promise; + override signOut( + options?: SignOutOptions, + sessionId?: string, + afterSignOut?: (afterSignOutUrl: string) => void, + ): Promise; + override async signOut(...args: any[]): Promise { + if (args[1] && typeof args[1] !== 'function') { + throw new Error('The second argument must be a function.'); + } + + const config: AsgardeoVueConfig = (await this.asgardeo.getConfigData()) as AsgardeoVueConfig; + + if (config.platform === Platform.AsgardeoV2) { + this.asgardeo.clearSession(); + + if (config.signInUrl) { + navigate(config.signInUrl); + } else { + this.signIn(config.signInOptions); + } + + args[1]?.(config.afterSignOutUrl || ''); + + return Promise.resolve(config.afterSignOutUrl || ''); + } + + const response: boolean = await this.asgardeo.signOut(args[1]); + + return Promise.resolve(String(response)); + } + + override async signUp(options?: SignUpOptions): Promise; + override async signUp(payload: EmbeddedFlowExecuteRequestPayload): Promise; + override async signUp(...args: any[]): Promise { + const config: AsgardeoVueConfig = (await this.asgardeo.getConfigData()) as AsgardeoVueConfig; + const firstArg: any = args[0]; + const baseUrl: string = config?.baseUrl; + + if (config.platform === Platform.AsgardeoV2) { + const authIdFromUrl: string = new URL(window.location.href).searchParams.get('authId'); + const authIdFromStorage: string = sessionStorage.getItem('asgardeo_auth_id'); + const authId: string = authIdFromUrl || authIdFromStorage; + + if (authIdFromUrl && !authIdFromStorage) { + sessionStorage.setItem('asgardeo_auth_id', authIdFromUrl); + } + + return executeEmbeddedSignUpFlowV2({ + authId, + baseUrl, + payload: + typeof firstArg === 'object' && 'flowType' in firstArg + ? {...(firstArg as EmbeddedFlowExecuteRequestPayload), verbose: true} + : (firstArg as EmbeddedFlowExecuteRequestPayload), + }) as any; + } + + if (typeof firstArg === 'object' && 'flowType' in firstArg) { + return executeEmbeddedSignUpFlow({ + baseUrl, + payload: firstArg as EmbeddedFlowExecuteRequestPayload, + }); + } + + navigate(getRedirectBasedSignUpUrl(config as Config)); + return undefined; + } + + async request(requestConfig?: HttpRequestConfig): Promise> { + return this.asgardeo.httpRequest(requestConfig); + } + + async requestAll(requestConfigs?: HttpRequestConfig[]): Promise[]> { + return this.asgardeo.httpRequestAll(requestConfigs); + } + + override async getAccessToken(sessionId?: string): Promise { + return this.asgardeo.getAccessToken(sessionId); + } + + override clearSession(sessionId?: string): void { + this.asgardeo.clearSession(sessionId); + } + + override async setSession(sessionData: Record, sessionId?: string): Promise { + return (await this.asgardeo.getStorageManager()).setSessionData(sessionData, sessionId); + } + + override decodeJwtToken>(token: string): Promise { + return this.asgardeo.decodeJwtToken(token); + } +} + +export default AsgardeoVueClient; diff --git a/packages/vue/src/__temp__/api.ts b/packages/vue/src/__temp__/api.ts new file mode 100644 index 000000000..95d8bf83e --- /dev/null +++ b/packages/vue/src/__temp__/api.ts @@ -0,0 +1,273 @@ +/** + * 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 { + AsgardeoSPAClient, + AuthClientConfig, + User, + LegacyConfig as Config, + IdToken, + Hooks, + HttpClientInstance, + HttpRequestConfig, + HttpResponse, + OIDCEndpoints, + SignInConfig, + SPACustomGrantConfig, +} from '@asgardeo/browser'; +import {AuthStateInterface} from './models'; + +class AuthAPI { + static DEFAULT_STATE: AuthStateInterface; + + private authState: AuthStateInterface = AuthAPI.DEFAULT_STATE; + + private client: AsgardeoSPAClient; + + private apiInstanceId: number; + + private loadingState: boolean; + + constructor(spaClient?: AsgardeoSPAClient, instanceId: number = 0) { + this.apiInstanceId = instanceId; + this.client = spaClient ?? AsgardeoSPAClient.getInstance(instanceId); + + this.getState = this.getState.bind(this); + this.init = this.init.bind(this); + this.signIn = this.signIn.bind(this); + this.signOut = this.signOut.bind(this); + this.updateState = this.updateState.bind(this); + } + + public getInstanceId(): number { + return this.apiInstanceId; + } + + public setLoadingState(isLoading: boolean): void { + this.loadingState = isLoading; + } + + public getLoadingState(): boolean { + return this.loadingState; + } + + public isLoading(): boolean { + return this.getLoadingState(); + } + + public getState(): AuthStateInterface { + return this.authState; + } + + public async init(config: AuthClientConfig): Promise { + return this.client.initialize(config); + } + + public async getConfigData(): Promise> { + return this.client.getConfigData(); + } + + public async getStorageManager(): Promise { + return this.client.getStorageManager(); + } + + public async isInitialized(): Promise { + return this.client.isInitialized(); + } + + public async signIn( + config: SignInConfig, + authorizationCode?: string, + sessionState?: string, + authState?: string, + callback?: (response: User) => void, + tokenRequestConfig?: { + params: Record; + }, + ): Promise { + return this.client + .signIn(config, authorizationCode, sessionState, authState, tokenRequestConfig) + .then(async (response: User) => { + if (!response) { + return null; + } + + if (await this.client.isSignedIn()) { + const stateToUpdate: AuthStateInterface = { + displayName: response.displayName, + email: response.email, + isLoading: false, + isSignedIn: true, + username: response.username, + }; + + this.updateState(stateToUpdate); + this.setLoadingState(false); + + if (callback) { + callback(response); + } + } + + return response; + }) + .catch((error: Error) => Promise.reject(error)); + } + + public signOut(callback?: (response?: boolean) => void): Promise { + return this.client + .signOut() + .then((response: boolean) => { + if (callback) { + callback(response); + } + + return response; + }) + .catch((error: Error) => Promise.reject(error)); + } + + public updateState(state: AuthStateInterface): void { + this.authState = {...this.authState, ...state}; + } + + public async getUser(): Promise { + return this.client.getUser(); + } + + public async httpRequest(config: HttpRequestConfig): Promise> { + return this.client.httpRequest(config); + } + + public async httpRequestAll(configs: HttpRequestConfig[]): Promise[]> { + return this.client.httpRequestAll(configs); + } + + public exchangeToken( + config: SPACustomGrantConfig, + callback: (response: User | Response) => void, + ): Promise { + return this.client + .exchangeToken(config) + .then((response: User | Response) => { + if (!response) { + return null; + } + + if (config.returnsSession) { + this.updateState({ + ...this.getState(), + ...(response as User), + isLoading: false, + isSignedIn: true, + }); + } + + if (callback) { + callback(response); + } + + return response; + }) + .catch((error: Error) => Promise.reject(error)); + } + + public async getOpenIDProviderEndpoints(): Promise { + return this.client.getOpenIDProviderEndpoints(); + } + + public async getHttpClient(): Promise { + return this.client.getHttpClient(); + } + + public async decodeJwtToken>(token: string): Promise { + return this.client.decodeJwtToken(token); + } + + public async getDecodedIdToken(sessionId?: string): Promise { + return this.client.getDecodedIdToken(sessionId); + } + + public async getIdToken(): Promise { + return this.client.getIdToken(); + } + + public async getAccessToken(sessionId?: string): Promise { + return this.client.getAccessToken(sessionId); + } + + public async refreshAccessToken(): Promise { + return this.client.refreshAccessToken(); + } + + public async isSignedIn(): Promise { + return this.client.isSignedIn(); + } + + public async enableHttpHandler(): Promise { + return this.client.enableHttpHandler(); + } + + public async disableHttpHandler(): Promise { + return this.client.disableHttpHandler(); + } + + public async reInitialize(config: Partial>): Promise { + return this.client.reInitialize(config); + } + + public on(hook: Hooks.CustomGrant, callback: (response?: any) => void, id: string): Promise; + public on(hook: Exclude, callback: (response?: any) => void): Promise; + public on(hook: Hooks, callback: (response?: any) => void, id?: string): Promise { + if (hook === Hooks.CustomGrant) { + return this.client.on(hook, callback, id); + } + + return this.client.on(hook, callback); + } + + public async signInSilently( + additionalParams?: Record, + tokenRequestConfig?: {params: Record}, + ): Promise { + return this.client + .signInSilently(additionalParams, tokenRequestConfig) + .then(async (response: User | boolean) => { + if (!response) { + return false; + } + + return response; + }) + .catch((error: Error) => Promise.reject(error)); + } + + public clearSession(sessionId?: string): void { + this.client.clearSession(sessionId); + } +} + +AuthAPI.DEFAULT_STATE = { + displayName: '', + email: '', + isLoading: true, + isSignedIn: false, + username: '', +}; + +export default AuthAPI; diff --git a/packages/vue/src/__temp__/models.ts b/packages/vue/src/__temp__/models.ts new file mode 100644 index 000000000..7da8ce972 --- /dev/null +++ b/packages/vue/src/__temp__/models.ts @@ -0,0 +1,44 @@ +/** + * 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. + */ + +/** + * Interface for the Authenticated state of the user which is exposed + * via `state` object from the auth context. + */ +export interface AuthStateInterface { + /** + * The display name of the user. + */ + displayName?: string; + /** + * The email address of the user. + */ + email?: string; + /** + * Are the Auth requests loading. + */ + isLoading: boolean; + /** + * Specifies if the user is authenticated or not. + */ + isSignedIn: boolean; + /** + * The username of the user. + */ + username?: string; +} diff --git a/packages/vue/src/api/getAllOrganizations.ts b/packages/vue/src/api/getAllOrganizations.ts new file mode 100644 index 000000000..439a1048e --- /dev/null +++ b/packages/vue/src/api/getAllOrganizations.ts @@ -0,0 +1,63 @@ +/** + * 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 { + HttpInstance, + HttpResponse, + AsgardeoSPAClient, + HttpRequestConfig, + getAllOrganizations as baseGetAllOrganizations, + GetAllOrganizationsConfig as BaseGetAllOrganizationsConfig, + AllOrganizationsApiResponse, +} from '@asgardeo/browser'; + +export interface GetAllOrganizationsConfig extends Omit { + fetcher?: (url: string, config: RequestInit) => Promise; + instanceId?: number; +} + +const getAllOrganizations = async ({ + fetcher, + instanceId = 0, + ...requestConfig +}: GetAllOrganizationsConfig): Promise => { + const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId); + const httpClient: HttpInstance = client.httpRequest.bind(client); + const response: HttpResponse = await httpClient({ + headers: config.headers as Record, + method: config.method || 'GET', + url, + } as HttpRequestConfig); + + return { + json: () => Promise.resolve(response.data), + ok: response.status >= 200 && response.status < 300, + status: response.status, + statusText: response.statusText || '', + text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), + } as Response; + }; + + return baseGetAllOrganizations({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default getAllOrganizations; diff --git a/packages/vue/src/api/getMeOrganizations.ts b/packages/vue/src/api/getMeOrganizations.ts new file mode 100644 index 000000000..9eba523a9 --- /dev/null +++ b/packages/vue/src/api/getMeOrganizations.ts @@ -0,0 +1,63 @@ +/** + * 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 { + Organization, + HttpInstance, + HttpResponse, + AsgardeoSPAClient, + HttpRequestConfig, + getMeOrganizations as baseGetMeOrganizations, + GetMeOrganizationsConfig as BaseGetMeOrganizationsConfig, +} from '@asgardeo/browser'; + +export interface GetMeOrganizationsConfig extends Omit { + fetcher?: (url: string, config: RequestInit) => Promise; + instanceId?: number; +} + +const getMeOrganizations = async ({ + fetcher, + instanceId = 0, + ...requestConfig +}: GetMeOrganizationsConfig): Promise => { + const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId); + const httpClient: HttpInstance = client.httpRequest.bind(client); + const response: HttpResponse = await httpClient({ + headers: config.headers as Record, + method: config.method || 'GET', + url, + } as HttpRequestConfig); + + return { + json: () => Promise.resolve(response.data), + ok: response.status >= 200 && response.status < 300, + status: response.status, + statusText: response.statusText || '', + text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), + } as Response; + }; + + return baseGetMeOrganizations({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default getMeOrganizations; diff --git a/packages/vue/src/api/getSchemas.ts b/packages/vue/src/api/getSchemas.ts new file mode 100644 index 000000000..8f90e270b --- /dev/null +++ b/packages/vue/src/api/getSchemas.ts @@ -0,0 +1,59 @@ +/** + * 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 { + Schema, + HttpInstance, + HttpResponse, + AsgardeoSPAClient, + HttpRequestConfig, + getSchemas as baseGetSchemas, + GetSchemasConfig as BaseGetSchemasConfig, +} from '@asgardeo/browser'; + +export interface GetSchemasConfig extends Omit { + fetcher?: (url: string, config: RequestInit) => Promise; + instanceId?: number; +} + +const getSchemas = async ({fetcher, instanceId = 0, ...requestConfig}: GetSchemasConfig): Promise => { + const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId); + const httpClient: HttpInstance = client.httpRequest.bind(client); + const response: HttpResponse = await httpClient({ + headers: config.headers as Record, + method: config.method || 'GET', + url, + } as HttpRequestConfig); + + return { + json: () => Promise.resolve(response.data), + ok: response.status >= 200 && response.status < 300, + status: response.status, + statusText: response.statusText || '', + text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), + } as Response; + }; + + return baseGetSchemas({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default getSchemas; diff --git a/packages/vue/src/api/getScim2Me.ts b/packages/vue/src/api/getScim2Me.ts new file mode 100644 index 000000000..c518b8655 --- /dev/null +++ b/packages/vue/src/api/getScim2Me.ts @@ -0,0 +1,59 @@ +/** + * 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 { + User, + HttpInstance, + HttpResponse, + AsgardeoSPAClient, + HttpRequestConfig, + getScim2Me as baseGetScim2Me, + GetScim2MeConfig as BaseGetScim2MeConfig, +} from '@asgardeo/browser'; + +export interface GetScim2MeConfig extends Omit { + fetcher?: (url: string, config: RequestInit) => Promise; + instanceId?: number; +} + +const getScim2Me = async ({fetcher, instanceId = 0, ...requestConfig}: GetScim2MeConfig): Promise => { + const defaultFetcher = async (url: string, config: RequestInit): Promise => { + const client: AsgardeoSPAClient = AsgardeoSPAClient.getInstance(instanceId); + const httpClient: HttpInstance = client.httpRequest.bind(client); + const response: HttpResponse = await httpClient({ + headers: config.headers as Record, + method: config.method || 'GET', + url, + } as HttpRequestConfig); + + return { + json: () => Promise.resolve(response.data), + ok: response.status >= 200 && response.status < 300, + status: response.status, + statusText: response.statusText || '', + text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), + } as Response; + }; + + return baseGetScim2Me({ + ...requestConfig, + fetcher: fetcher || defaultFetcher, + }); +}; + +export default getScim2Me; diff --git a/packages/vue/src/auth-api.ts b/packages/vue/src/auth-api.ts deleted file mode 100644 index cd57c4fcb..000000000 --- a/packages/vue/src/auth-api.ts +++ /dev/null @@ -1,435 +0,0 @@ -/** - * 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 { - AsgardeoAuthException, - AsgardeoSPAClient, - AuthClientConfig, - BasicUserInfo, - Config, - IdToken, - Hooks, - HttpClientInstance, - HttpRequestConfig, - HttpResponse, - OIDCEndpoints, - SignInConfig, - SPACustomGrantConfig, -} from '@asgardeo/auth-spa'; -import {reactive} from 'vue'; -import {AuthStateInterface, AuthVueConfig} from './types'; - -class AuthAPI { - static DEFAULT_STATE: AuthStateInterface; - - private _authState: AuthStateInterface = reactive({...AuthAPI.DEFAULT_STATE}); - - private _client: AsgardeoSPAClient; - - constructor(spaClient?: AsgardeoSPAClient) { - this._client = spaClient ?? AsgardeoSPAClient.getInstance(); - } - - /** - * Method to return Auth Client instance authentication state. - * - * @return {AuthStateInterface} Authentication State. - */ - public getState = (): AuthStateInterface => this._authState; - - /** - * Initializes the AuthClient instance with the given authentication configuration. - * - * @param {AuthClientConfig} config - The authentication configuration object - * containing details such as client ID, redirect URLs, and base URL. - * @returns {Promise} A promise that resolves to `true` if initialization is successful. - */ - public init = (config: AuthVueConfig): Promise => this._client.initialize(config); - - /** - * Handles user sign-in by exchanging the authorization code for tokens - * and updating the authentication state if the user is authenticated. - * - * @param {SignInConfig} config - The sign-in configuration containing client-specific settings. - * @param {string} authorizationCode - The authorization code received from the authentication provider. - * @param {string} sessionState - The session state value to track the authentication session. - * @param {string} [authState] - An optional authentication state parameter for additional tracking. - * @param {{ params: Record }} [tokenRequestConfig] - Optional token request parameters. - * @returns {Promise} A promise resolving to the authenticated user's basic information. - */ - public signIn = async ( - config?: SignInConfig, - authorizationCode?: string, - sessionState?: string, - authState?: string, - callback?: (response: BasicUserInfo) => void, - tokenRequestConfig?: {params: Record}, - ): Promise => - this._client - .signIn(config, authorizationCode, sessionState, authState, tokenRequestConfig) - .then(async (response: BasicUserInfo) => { - if (!response) { - return response; - } - if (await this._client.isSignedIn()) { - Object.assign(this._authState, { - allowedScopes: response.allowedScopes, - displayName: response.displayName, - email: response.email, - isLoading: false, - isSignedIn: true, - isSigningOut: false, - sub: response.sub, - username: response.username, - }); - - if (callback) { - callback(response); - } - } - - return response; - }) - .catch((error: Error) => Promise.reject(error)); - - /** - * Signs the user out and resets the authentication state. - * - * @param {(response?: boolean) => void} callback - An optional callback function to execute after sign-out. - * @returns {Promise} A promise resolving to `true` if sign-out is successful. - * - */ - public signOut = async (callback?: (response?: boolean) => void): Promise => - this._client - .signOut() - .then((response: boolean) => { - if (callback) { - callback(response); - } - return response; - }) - .catch((error: AsgardeoAuthException) => Promise.reject(error)); - - /** - * Method to update Auth Client instance authentication state. - * - * @param {AuthStateInterface} state - State values to update in authentication state. - */ - public updateState(state: AuthStateInterface): void { - this._authState = {...this._authState, ...state}; - } - - /** - * This method returns a Promise that resolves with the basic user information obtained from the ID token. - * - * @return {Promise} a promise that resolves with the user information. - */ - public async getBasicUserInfo(): Promise { - return this._client.getBasicUserInfo(); - } - - /** - * This method returns a Promise that resolves with the basic user information obtained from the ID token. - * - * @return {Promise} a promise that resolves with the user information. - */ - public async getUser(): Promise { - return this._client.getBasicUserInfo(); - } - - /** - * This method sends an API request to a protected endpoint. - * The access token is automatically attached to the header of the request. - * This is the only way by which protected endpoints can be accessed - * when the web worker is used to store session information. - * - * @param {HttpRequestConfig} config - The config object containing attributes necessary to send a request. - * - * @return {Promise} - Returns a Promise that resolves with the response to the request. - */ - public async httpRequest(config: HttpRequestConfig): Promise> { - return this._client.httpRequest(config); - } - - /** - * This method sends multiple API requests to a protected endpoint. - * The access token is automatically attached to the header of the request. - * This is the only way by which multiple requests can be sent to protected endpoints - * when the web worker is used to store session information. - * - * @param {HttpRequestConfig[]} configs - The config object containing attributes necessary to send a request. - * - * @return {Promise} a Promise that resolves with the responses to the requests. - */ - public async httpRequestAll(configs: HttpRequestConfig[]): Promise[]> { - return this._client.httpRequestAll(configs); - } - - /** - * This method allows you to send a request with a custom grant. - * - * @param {CustomGrantRequestParams} config - The request parameters. - * @param {(response: BasicUserInfo | Response) => void} [callback] - An optional callback function. - * - * @return {Promise} a promise that resolves with - * the value returned by the custom grant request. - */ - public exchangeToken( - config: SPACustomGrantConfig, - callback?: (response: BasicUserInfo | Response) => void, - ): Promise { - return this._client - .exchangeToken(config) - .then((response: BasicUserInfo | Response) => { - if (!response) { - return response; - } - - if (config.returnsSession) { - Object.assign(this._authState, { - ...this._authState, - ...(response as BasicUserInfo), - isLoading: false, - isSignedIn: true, - }); - } - if (callback) { - callback(response); - } - return response; - }) - .catch((error: AsgardeoAuthException) => Promise.reject(error)); - } - - /** - * This method ends a user session. The access token is revoked and the session information is destroyed. - * - * @return {Promise} - A promise that resolves with `true` if the process is successful. - */ - public async revokeAccessToken(): Promise { - return this._client - .revokeAccessToken() - .then(() => { - this._authState = {...AuthAPI.DEFAULT_STATE, isLoading: false}; - return true; - }) - .catch((error: AsgardeoAuthException) => Promise.reject(error)); - } - - /** - * This method returns a Promise that resolves with an object containing the service endpoints. - * - * @return {Promise} - A Promise that resolves with an object containing the service endpoints. - */ - public async getOpenIDProviderEndpoints(): Promise { - return this._client.getOpenIDProviderEndpoints(); - } - - /** - * This methods returns the Axios http client. - * - * @return {HttpClientInstance} - The Axios HTTP client. - */ - public async getHttpClient(): Promise { - return this._client.getHttpClient(); - } - - /** - * This method decodes the payload of the id token and returns it. - * - * @return {Promise} - A Promise that resolves with - * the decoded payload of the id token. - */ - public async getDecodedIdToken(): Promise { - return this._client.getDecodedIdToken(); - } - - /** - * This method decodes the payload of the idp id token and returns it. - * @remarks - * This method is intended for retrieving the IdP ID token when extending a plugin. - * - * @return {Promise} - A Promise that resolves with - * the decoded payload of the idp id token. - */ - public async getDecodedIDPIDToken(): Promise { - return this._client.getDecodedIdToken(); - } - - /** - * This method returns the ID token. - * - * @return {Promise} - A Promise that resolves with the id token. - */ - public async getIdToken(): Promise { - return this._client.getIdToken(); - } - - /** - * This method return a Promise that resolves with the access token. - * - * @remarks - * This method will not return the access token if the storage type is set to `webWorker`. - * - * @return {Promise} - A Promise that resolves with the access token. - */ - public getAccessToken = async (): Promise => this._client.getAccessToken(); - - /** - * This method returns a Promise that resolves with the IDP access token. - * - * @remarks - * This method will not return the IDP access token if the storage type is set to `webWorker`. - * It can be used to access the IDP access token when custom authentication grant functionalities are used. - * - * @return {Promise} A Promise that resolves with the IDP access token. - */ - public async getIDPAccessToken(): Promise { - return this._client.getIDPAccessToken(); - } - - /** - * This method refreshes the access token. - * - * @return {BasicUserInfo} - A Promise that resolves with an object containing - * information about the refreshed access token. - */ - public async refreshAccessToken(): Promise { - return this._client.refreshAccessToken(); - } - - /** - * This method specifies if the user is authenticated or not. - * - * @return {Promise} - A Promise that resolves with `true` if the user is authenticated. - */ - public async isSignedIn(): Promise { - return this._client.isSignedIn(); - } - - /** - * This method specifies if the session is active or not. - * - * @return {Promise} - A Promise that resolves with `true` if there is an active session. - */ - public async isSessionActive(): Promise { - return this._client.isSessionActive(); - } - - /** - * This method enables callback functions attached to the http client. - * - * @return {Promise} - A promise that resolves with `true`. - */ - public async enableHttpHandler(): Promise { - return this._client.enableHttpHandler(); - } - - /** - * This method disables callback functions attached to the http client. - * - * @return {Promise} - A promise that resolves with `true`. - */ - public async disableHttpHandler(): Promise { - return this._client.disableHttpHandler(); - } - - /** - * This method updates the configuration that was passed into the constructor when instantiating this class. - * - * @param {Partial>} config - A config object to update the SDK configurations with. - */ - public async reInitialize(config: Partial>): Promise { - return this._client.reInitialize(config); - } - - /** - * This method attaches a callback function to an event hook that fires the callback when the event happens. - * - * @param {Hooks.CustomGrant} hook - The name of the hook. - * @param {(response?: any) => void} callback - The callback function. - * @param {string} id- Optional id for the hook. This is used when multiple custom grants are used. - * - */ - public on(hook: Hooks.CustomGrant, callback: (response?: any) => void, id: string): Promise; - public on(hook: Exclude, callback: (response?: any) => void): Promise; - public on(hook: Hooks, callback: (response?: any) => void, id?: string): Promise { - if (hook === Hooks.CustomGrant) { - return this._client.on(hook, callback, id); - } - - return this._client.on(hook, callback); - } - - /** - * This method allows you to sign in silently. - * First, this method sends a prompt-none request to check for an active user session in the identity provider. - * If a session exists, it retrieves the access token and stores it. Otherwise, it returns `false`. - * - * @param {Record} [additionalParams] - Optional additional parameters to be sent with the request. - * @param {{ params: Record }} [tokenRequestConfig] - Optional configuration for the token request. - * - * @returns {Promise} A Promise that resolves with the user information after signing in, - * or `false` if the user is not signed in. - * - * @example - * ``` - * client.signInSilently(); - * ``` - */ - public async signInSilently( - additionalParams?: Record, - tokenRequestConfig?: {params: Record}, - ): Promise { - return this._client - .signInSilently(additionalParams, tokenRequestConfig) - .then(async (response: BasicUserInfo | boolean) => { - if (!response) { - Object.assign(this._authState, {isLoading: false}); - return false; - } - - if (await this._client.isSignedIn()) { - const basicUserInfo: BasicUserInfo = response as BasicUserInfo; - Object.assign(this._authState, { - allowedScopes: basicUserInfo.allowedScopes, - displayName: basicUserInfo.displayName, - email: basicUserInfo.email, - isLoading: false, - isSignedIn: true, - sub: basicUserInfo.sub, - username: basicUserInfo.username, - }); - } - return response; - }) - .catch((error: AsgardeoAuthException) => Promise.reject(error)); - } -} - -AuthAPI.DEFAULT_STATE = { - allowedScopes: '', - displayName: '', - email: '', - isLoading: true, - isSignedIn: false, - sub: '', - username: '', -}; - -export default AuthAPI; diff --git a/packages/vue/src/components/actions/BaseSignInButton.ts b/packages/vue/src/components/actions/BaseSignInButton.ts new file mode 100644 index 000000000..ef7e28987 --- /dev/null +++ b/packages/vue/src/components/actions/BaseSignInButton.ts @@ -0,0 +1,104 @@ +/** + * 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 {withVendorCSSClassPrefix} from '@asgardeo/browser'; +import {type VNode, defineComponent, h} from 'vue'; +import Button from '../primitives/Button'; + +/** + * BaseSignInButton — styled sign-in button with customization support. + * + * By default, renders a styled Button primitive with contents from the slot or fallback text. + * Set `unstyled={true}` to render a plain