diff --git a/.changeset/ten-aliens-know.md b/.changeset/ten-aliens-know.md new file mode 100644 index 000000000..52da6682f --- /dev/null +++ b/.changeset/ten-aliens-know.md @@ -0,0 +1,8 @@ +--- +'@asgardeo/browser': patch +'@asgardeo/nextjs': patch +'@asgardeo/react': patch +'@asgardeo/node': patch +--- + +Fix lint issues in packages diff --git a/e2e/playwright-report-embedded/index.html b/e2e/playwright-report-embedded/index.html index d0a874934..5a1660df4 100644 --- a/e2e/playwright-report-embedded/index.html +++ b/e2e/playwright-report-embedded/index.html @@ -82,4 +82,4 @@
- \ No newline at end of file + \ No newline at end of file diff --git a/package.json b/package.json index 1be180174..2ce59a80d 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,11 @@ "seroval": "1.4.1", "qs": "6.14.1", "@vitejs/plugin-vue>vite": "7.1.12", - "prettier": "2.6.2" + "prettier": "2.6.2", + "@wso2/eslint-plugin>@typescript-eslint/eslint-plugin": "6.21.0", + "@wso2/eslint-plugin>@typescript-eslint/parser": "6.21.0", + "@wso2/eslint-plugin>@typescript-eslint/type-utils": "6.21.0", + "@wso2/eslint-plugin>@typescript-eslint/utils": "6.21.0" } }, "publishConfig": { diff --git a/packages/browser/.eslintignore b/packages/browser/.eslintignore index 177586b6b..ca1d662bb 100644 --- a/packages/browser/.eslintignore +++ b/packages/browser/.eslintignore @@ -1,4 +1,5 @@ /dist /build /node_modules -/coverage \ No newline at end of file +/coverage +/src/__legacy__ diff --git a/packages/browser/esbuild.config.mjs b/packages/browser/esbuild.config.mjs index 9cdfbc978..90d81e359 100644 --- a/packages/browser/esbuild.config.mjs +++ b/packages/browser/esbuild.config.mjs @@ -16,88 +16,89 @@ * under the License. */ -import { readFileSync } from 'fs'; +import {readFileSync} from 'fs'; +import {createRequire} from 'module'; import * as esbuild from 'esbuild'; -import { createRequire } from 'module'; import inlineWorkerPlugin from 'esbuild-plugin-inline-worker'; const require = createRequire(import.meta.url); const pkg = JSON.parse(readFileSync('./package.json', 'utf8')); // Get dependencies excluding crypto-related ones that need to be bundled -const externalDeps = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})] - .filter(dep => !['crypto-browserify', 'randombytes', 'buffer'].includes(dep)); +const externalDeps = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})].filter( + dep => !['crypto-browserify', 'randombytes', 'buffer'].includes(dep), +); // Plugin to alias crypto and buffer modules const polyfillPlugin = { name: 'polyfill-plugin', setup(build) { // Crypto polyfill - build.onResolve({ filter: /^crypto$/ }, () => ({ - path: require.resolve('crypto-browserify') + build.onResolve({filter: /^crypto$/}, () => ({ + path: require.resolve('crypto-browserify'), })); // Buffer polyfill - build.onResolve({ filter: /^buffer$/ }, () => ({ - path: require.resolve('buffer/') + build.onResolve({filter: /^buffer$/}, () => ({ + path: require.resolve('buffer/'), })); - } + }, }; const commonOptions = { - bundle: true, - entryPoints: ['src/index.ts'], - external: externalDeps, - platform: 'browser', - target: ['es2020'], - define: { - global: 'globalThis', // Required by crypto-browserify - 'process.env.NODE_DEBUG': 'false', - 'process.version': '"16.0.0"', - 'process.browser': 'true' - }, banner: { js: ` import { Buffer } from 'buffer/'; if (typeof window !== 'undefined' && !window.Buffer) { window.Buffer = Buffer; } - ` + `, + }, + bundle: true, + define: { + global: 'globalThis', // Required by crypto-browserify + 'process.browser': 'true', + 'process.env.NODE_DEBUG': 'false', + 'process.version': '"16.0.0"', }, + entryPoints: ['src/index.ts'], + external: externalDeps, footer: { js: ` if (typeof window !== 'undefined' && !window.Buffer) { window.Buffer = require('buffer/').Buffer; } - ` + `, }, + platform: 'browser', plugins: [ polyfillPlugin, inlineWorkerPlugin({ - format: 'iife', - target: 'es2020', - platform: 'browser', define: { - 'global': 'self', - 'globalThis': 'self', + global: 'self', + globalThis: 'self', + 'process.browser': 'true', 'process.env.NODE_DEBUG': 'false', 'process.version': '"16.0.0"', - 'process.browser': 'true' - } - }) - ] + }, + format: 'iife', + platform: 'browser', + target: 'es2020', + }), + ], + target: ['es2020'], }; await esbuild.build({ ...commonOptions, format: 'esm', outfile: 'dist/index.js', - sourcemap: true + sourcemap: true, }); await esbuild.build({ ...commonOptions, format: 'cjs', outfile: 'dist/cjs/index.js', - sourcemap: true + sourcemap: true, }); diff --git a/packages/browser/src/__legacy__/client.ts b/packages/browser/src/__legacy__/client.ts index a015eb96c..54f47b543 100755 --- a/packages/browser/src/__legacy__/client.ts +++ b/packages/browser/src/__legacy__/client.ts @@ -26,6 +26,7 @@ import { IdToken, OIDCEndpoints, User, + createPackageComponentLogger, } from '@asgardeo/javascript'; import WorkerFile from '../web.worker'; import {MainThreadClient, WebWorkerClient} from './clients'; @@ -47,6 +48,11 @@ import { import {BrowserStorage} from './models/storage'; import {SPAUtils} from './utils'; +const logger: ReturnType = createPackageComponentLogger( + '@asgardeo/browser', + 'AsgardeoSPAClient', +); + /** * Default configurations. */ @@ -85,7 +91,7 @@ export class AsgardeoSPAClient { this._instanceID = id; } - public instantiateAuthHelper(authHelper?: typeof AuthenticationHelper) { + public instantiateAuthHelper(authHelper?: typeof AuthenticationHelper): void { if (authHelper) { this._authHelper = authHelper; } else { @@ -93,7 +99,7 @@ export class AsgardeoSPAClient { } } - public instantiateWorker(worker: new () => Worker) { + public instantiateWorker(worker: new () => Worker): void { if (worker) { this._worker = worker; } else { @@ -123,8 +129,7 @@ export class AsgardeoSPAClient { while (!this._initialized) { if (iterationToWait === 1e4) { - // eslint-disable-next-line no-console - console.warn('It is taking longer than usual for the object to be initialized'); + logger.warn('It is taking longer than usual for the object to be initialized'); } await sleep(); iterationToWait++; diff --git a/packages/browser/src/__legacy__/http-client/helpers/decorators.ts b/packages/browser/src/__legacy__/http-client/helpers/decorators.ts index 16be28c98..0a635c307 100644 --- a/packages/browser/src/__legacy__/http-client/helpers/decorators.ts +++ b/packages/browser/src/__legacy__/http-client/helpers/decorators.ts @@ -17,14 +17,11 @@ * */ -/* eslint-disable @typescript-eslint/no-empty-function */ -/* eslint-disable @typescript-eslint/no-unused-vars */ - /** * A decorator to supplement static interface support. * * @return {(constructor: U) => void} */ export function staticDecorator() { - return (_constructor: U): any => {}; + return (constructor: U): U => constructor; } diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 5b687a171..4f3b8e45a 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -19,10 +19,13 @@ /** * Entry point for all public APIs of this SDK. */ +// eslint-disable-next-line import/no-cycle export * from './__legacy__/client'; +// eslint-disable-next-line import/no-cycle export * from './__legacy__/models'; // Utils +// eslint-disable-next-line import/no-cycle export * from './__legacy__/utils/spa-utils'; // Constants diff --git a/packages/browser/src/theme/themeDetection.ts b/packages/browser/src/theme/themeDetection.ts index 5c47329ec..b85e80ee0 100644 --- a/packages/browser/src/theme/themeDetection.ts +++ b/packages/browser/src/theme/themeDetection.ts @@ -16,7 +16,12 @@ * under the License. */ -import {ThemeDetection, ThemeMode} from '@asgardeo/javascript'; +import {ThemeDetection, ThemeMode, createPackageComponentLogger} from '@asgardeo/javascript'; + +const logger: ReturnType = createPackageComponentLogger( + '@asgardeo/browser', + 'ThemeDetection', +); /** * Extended theme detection config that includes DOM-specific options @@ -51,11 +56,11 @@ export const detectThemeMode = (mode: ThemeMode, config: BrowserThemeDetection = if (mode === 'class') { if (!targetElement) { - console.warn('ThemeDetection: targetElement is required for class-based detection, falling back to light mode'); + logger.warn('ThemeDetection: targetElement is required for class-based detection, falling back to light mode'); return 'light'; } - const classList = targetElement.classList; + const {classList} = targetElement; // Check for explicit dark class first if (classList.contains(darkClass)) { @@ -84,10 +89,10 @@ export const createClassObserver = ( ): MutationObserver => { const {darkClass = 'dark', lightClass = 'light'} = config; - const observer = new MutationObserver(mutations => { - mutations.forEach(mutation => { + const observer: MutationObserver = new MutationObserver((mutations: MutationRecord[]) => { + mutations.forEach((mutation: MutationRecord) => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { - const classList = targetElement.classList; + const {classList} = targetElement; if (classList.contains(darkClass)) { callback(true); @@ -101,8 +106,8 @@ export const createClassObserver = ( }); observer.observe(targetElement, { - attributes: true, attributeFilter: ['class'], + attributes: true, }); return observer; @@ -116,9 +121,9 @@ export const createMediaQueryListener = (callback: (isDark: boolean) => void): M return null; } - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const mediaQuery: MediaQueryList = window.matchMedia('(prefers-color-scheme: dark)'); - const handleChange = (e: MediaQueryListEvent) => { + const handleChange = (e: MediaQueryListEvent): void => { callback(e.matches); }; diff --git a/packages/browser/src/types/worker.d.ts b/packages/browser/src/types/worker.d.ts index fea59dc87..bbee86c69 100644 --- a/packages/browser/src/types/worker.d.ts +++ b/packages/browser/src/types/worker.d.ts @@ -18,35 +18,35 @@ // Type declarations for worker files handled by esbuild-plugin-inline-worker -declare module "*.worker" { +declare module '*.worker' { const WorkerFactory: { new (): Worker; }; export default WorkerFactory; } -declare module "*.worker.js" { +declare module '*.worker.js' { const WorkerFactory: { new (): Worker; }; export default WorkerFactory; } -declare module "*.worker.ts" { +declare module '*.worker.ts' { const WorkerFactory: { new (): Worker; }; export default WorkerFactory; } -declare module "*.worker.jsx" { +declare module '*.worker.jsx' { const WorkerFactory: { new (): Worker; }; export default WorkerFactory; } -declare module "*.worker.tsx" { +declare module '*.worker.tsx' { const WorkerFactory: { new (): Worker; }; diff --git a/packages/browser/src/utils/__tests__/navigate.test.ts b/packages/browser/src/utils/__tests__/navigate.test.ts index 968b6dd9a..39ec26fbf 100644 --- a/packages/browser/src/utils/__tests__/navigate.test.ts +++ b/packages/browser/src/utils/__tests__/navigate.test.ts @@ -24,7 +24,7 @@ import {vi, describe, it, expect, beforeEach, afterEach} from 'vitest'; import navigate from '../navigate'; describe('navigate', () => { - const originalLocation = window.location; + const originalLocation: Location = window.location; beforeEach(() => { // @ts-ignore @@ -37,8 +37,8 @@ describe('navigate', () => { window.location = { ...originalLocation, assign: vi.fn(), - origin: 'https://localhost:5173', href: 'https://localhost:5173/', + origin: 'https://localhost:5173', }; }); @@ -58,15 +58,15 @@ describe('navigate', () => { navigate('/test-url'); expect(window.dispatchEvent).toHaveBeenCalledWith( expect.objectContaining({ - type: 'popstate', state: null, + type: 'popstate', }), ); expect(window.location.assign).not.toHaveBeenCalled(); }); it('should use window.location.assign for cross-origin URLs', () => { - const crossOriginUrl = 'https://accounts.asgardeo.io/t/dxlab/accountrecoveryendpoint/register.do'; + const crossOriginUrl: string = 'https://accounts.asgardeo.io/t/dxlab/accountrecoveryendpoint/register.do'; navigate(crossOriginUrl); expect(window.location.assign).toHaveBeenCalledWith(crossOriginUrl); expect(window.history.pushState).not.toHaveBeenCalled(); @@ -74,7 +74,7 @@ describe('navigate', () => { }); it('should use window.location.assign for malformed URLs', () => { - const malformedUrl = 'http://[::1'; // Invalid URL + const malformedUrl: string = 'http://[::1'; // Invalid URL navigate(malformedUrl); expect(window.location.assign).toHaveBeenCalledWith(malformedUrl); expect(window.history.pushState).not.toHaveBeenCalled(); diff --git a/packages/browser/src/utils/handleWebAuthnAuthentication.ts b/packages/browser/src/utils/handleWebAuthnAuthentication.ts index d7148f60e..f2104510d 100644 --- a/packages/browser/src/utils/handleWebAuthnAuthentication.ts +++ b/packages/browser/src/utils/handleWebAuthnAuthentication.ts @@ -16,7 +16,17 @@ * under the License. */ -import {arrayBufferToBase64url, base64urlToArrayBuffer, AsgardeoRuntimeError} from '@asgardeo/javascript'; +import { + arrayBufferToBase64url, + base64urlToArrayBuffer, + AsgardeoRuntimeError, + createPackageComponentLogger, +} from '@asgardeo/javascript'; + +const logger: ReturnType = createPackageComponentLogger( + '@asgardeo/browser', + 'WebAuthn', +); /** * Handles WebAuthn/Passkey authentication flow for browser environments. @@ -116,23 +126,23 @@ const handleWebAuthnAuthentication = async (challengeData: string): Promise { try { - const targetUrl = new URL(url, window.location.origin); + const targetUrl: URL = new URL(url, window.location.origin); if (targetUrl.origin === window.location.origin) { window.history.pushState(null, '', targetUrl.pathname + targetUrl.search + targetUrl.hash); window.dispatchEvent(new PopStateEvent('popstate', {state: null})); diff --git a/packages/browser/src/web.worker.ts b/packages/browser/src/web.worker.ts index ee3f00852..f4981ba9c 100644 --- a/packages/browser/src/web.worker.ts +++ b/packages/browser/src/web.worker.ts @@ -1,7 +1,7 @@ /** - * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * 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 @@ -16,12 +16,14 @@ * under the License. */ -import { Buffer } from 'buffer/'; import {AsgardeoAuthClient} from '@asgardeo/javascript'; +import {Buffer} from 'buffer/'; +// eslint-disable-next-line import/no-cycle import {AuthenticationHelper, SPAHelper} from './__legacy__/helpers'; import {WebWorkerClientConfig} from './__legacy__/models'; import {workerReceiver} from './__legacy__/worker/worker-receiver'; +/* eslint-disable no-restricted-globals */ // Set up global polyfills if (typeof self !== 'undefined' && !(self as any).Buffer) { (self as any).Buffer = Buffer; @@ -34,9 +36,11 @@ if (typeof self !== 'undefined') { // Note: globalThis is read-only, so we don't try to override it // The esbuild config already maps globalThis to self via define } +/* eslint-enable no-restricted-globals */ -workerReceiver((authClient: AsgardeoAuthClient, spaHelper: SPAHelper) => { - return new AuthenticationHelper(authClient, spaHelper); -}); +workerReceiver( + (authClient: AsgardeoAuthClient, spaHelper: SPAHelper) => + new AuthenticationHelper(authClient, spaHelper), +); export default {} as typeof Worker & {new (): Worker}; diff --git a/packages/browser/vitest.config.ts b/packages/browser/vitest.config.ts index 44df54fb2..24ef9e162 100644 --- a/packages/browser/vitest.config.ts +++ b/packages/browser/vitest.config.ts @@ -16,13 +16,14 @@ * under the License. */ +// eslint-disable-next-line import/no-extraneous-dependencies import {defineConfig} from 'vitest/config'; export default defineConfig({ test: { environment: 'jsdom', globals: true, - testTimeout: 10000, hookTimeout: 10000, + testTimeout: 10000, }, }); diff --git a/packages/nextjs/.eslintrc.cjs b/packages/nextjs/.eslintrc.cjs index a835778cd..71ced7061 100644 --- a/packages/nextjs/.eslintrc.cjs +++ b/packages/nextjs/.eslintrc.cjs @@ -36,4 +36,11 @@ module.exports = { project: [path.resolve(__dirname, 'tsconfig.eslint.json')], }, plugins: ['@wso2'], + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx', '.d.ts', '.mjs', '.json'], + }, + }, + }, }; diff --git a/packages/nextjs/src/AsgardeoNextClient.ts b/packages/nextjs/src/AsgardeoNextClient.ts index 093eff24c..537871777 100644 --- a/packages/nextjs/src/AsgardeoNextClient.ts +++ b/packages/nextjs/src/AsgardeoNextClient.ts @@ -17,49 +17,51 @@ */ import { + AllOrganizationsApiResponse, AsgardeoNodeClient, AsgardeoRuntimeError, + AuthClientConfig, + CreateOrganizationPayload, + EmbeddedFlowExecuteRequestConfig, EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteResponse, + EmbeddedSignInFlowHandleRequestPayload, + ExtendedAuthorizeRequestUrlParams, + FlattenedSchema, + IdToken, LegacyAsgardeoNodeClient, + Organization, + OrganizationDetails, + Schema, SignInOptions, SignOutOptions, SignUpOptions, + Storage, + TokenExchangeRequestConfig, + TokenResponse, User, UserProfile, - initializeEmbeddedSignInFlow, - Organization, - EmbeddedSignInFlowHandleRequestPayload, + createOrganization, + deriveOrganizationHandleFromBaseUrl, executeEmbeddedSignInFlow, - EmbeddedFlowExecuteRequestConfig, - ExtendedAuthorizeRequestUrlParams, - generateUserProfile, + executeEmbeddedSignUpFlow, + extractUserClaimsFromIdToken, flattenUserSchema, - getScim2Me, - getSchemas, generateFlattenedUserProfile, - updateMeProfile, - executeEmbeddedSignUpFlow, + generateUserProfile, + getAllOrganizations, getMeOrganizations, - IdToken, - createOrganization, - CreateOrganizationPayload, getOrganization, - OrganizationDetails, - deriveOrganizationHandleFromBaseUrl, - getAllOrganizations, - AllOrganizationsApiResponse, - extractUserClaimsFromIdToken, - TokenResponse, - Storage, - TokenExchangeRequestConfig, + getScim2Me, + getSchemas, + initializeEmbeddedSignInFlow, + updateMeProfile, } from '@asgardeo/node'; import {AsgardeoNextConfig} from './models/config'; +import getClientOrigin from './server/actions/getClientOrigin'; import getSessionId from './server/actions/getSessionId'; import decorateConfigWithNextEnv from './utils/decorateConfigWithNextEnv'; -import getClientOrigin from './server/actions/getClientOrigin'; -const removeTrailingSlash = (path: string): string => (path.endsWith('/') ? path.slice(0, -1) : path); /** * Client for mplementing Asgardeo in Next.js applications. * This class provides the core functionality for managing user authentication and sessions. @@ -70,7 +72,9 @@ const removeTrailingSlash = (path: string): string => (path.endsWith('/') ? path */ class AsgardeoNextClient extends AsgardeoNodeClient { private static instance: AsgardeoNextClient; + private asgardeo: LegacyAsgardeoNodeClient; + public isInitialized: boolean = false; private constructor() { @@ -130,15 +134,15 @@ class AsgardeoNextClient exte return this.asgardeo.initialize( { - organizationHandle: resolvedOrganizationHandle, + afterSignInUrl: afterSignInUrl ?? origin, + afterSignOutUrl: afterSignOutUrl ?? origin, baseUrl, clientId, clientSecret, + enablePKCE: false, + organizationHandle: resolvedOrganizationHandle, signInUrl, signUpUrl, - afterSignInUrl: afterSignInUrl ?? origin, - afterSignOutUrl: afterSignOutUrl ?? origin, - enablePKCE: false, ...rest, } as any, storage, @@ -169,17 +173,17 @@ class AsgardeoNextClient exte const resolvedSessionId: string = userId || ((await getSessionId()) as string); try { - const configData = await this.asgardeo.getConfigData(); - const baseUrl = configData?.baseUrl; + const configData: AuthClientConfig = await this.asgardeo.getConfigData(); + const baseUrl: string | undefined = configData?.baseUrl; - const profile = await getScim2Me({ + const profile: User = await getScim2Me({ baseUrl, headers: { Authorization: `Bearer ${await this.getAccessToken(userId)}`, }, }); - const schemas = await getSchemas({ + const schemas: Schema[] = await getSchemas({ baseUrl, headers: { Authorization: `Bearer ${await this.getAccessToken(userId)}`, @@ -196,37 +200,37 @@ class AsgardeoNextClient exte await this.ensureInitialized(); try { - const configData = await this.asgardeo.getConfigData(); - const baseUrl = configData?.baseUrl; + const configData: AuthClientConfig = await this.asgardeo.getConfigData(); + const baseUrl: string | undefined = configData?.baseUrl; - const profile = await getScim2Me({ + const profile: User = await getScim2Me({ baseUrl, headers: { Authorization: `Bearer ${await this.getAccessToken(userId)}`, }, }); - const schemas = await getSchemas({ + const schemas: Schema[] = await getSchemas({ baseUrl, headers: { Authorization: `Bearer ${await this.getAccessToken(userId)}`, }, }); - const processedSchemas = flattenUserSchema(schemas); + const processedSchemas: FlattenedSchema[] = flattenUserSchema(schemas); - const output = { - schemas: processedSchemas, + const output: UserProfile = { flattenedProfile: generateFlattenedUserProfile(profile, processedSchemas), profile, + schemas: processedSchemas, }; return output; } catch (error) { return { - schemas: [], flattenedProfile: extractUserClaimsFromIdToken(await this.asgardeo.getDecodedIdToken(userId)), profile: extractUserClaimsFromIdToken(await this.asgardeo.getDecodedIdToken(userId)), + schemas: [], }; } } @@ -235,15 +239,15 @@ class AsgardeoNextClient exte await this.ensureInitialized(); try { - const configData = await this.asgardeo.getConfigData(); - const baseUrl = configData?.baseUrl; + const configData: AuthClientConfig = await this.asgardeo.getConfigData(); + const baseUrl: string | undefined = configData?.baseUrl; return await updateMeProfile({ baseUrl, - payload, headers: { Authorization: `Bearer ${await this.getAccessToken(userId)}`, }, + payload, }); } catch (error) { throw new AsgardeoRuntimeError( @@ -257,15 +261,15 @@ class AsgardeoNextClient exte async createOrganization(payload: CreateOrganizationPayload, userId?: string): Promise { try { - const configData = await this.asgardeo.getConfigData(); + const configData: AuthClientConfig = await this.asgardeo.getConfigData(); const baseUrl: string = configData?.baseUrl as string; return await createOrganization({ - payload, baseUrl, headers: { Authorization: `Bearer ${await this.getAccessToken(userId)}`, }, + payload, }); } catch (error) { throw new AsgardeoRuntimeError( @@ -279,15 +283,15 @@ class AsgardeoNextClient exte async getOrganization(organizationId: string, userId?: string): Promise { try { - const configData = await this.asgardeo.getConfigData(); + const configData: AuthClientConfig = await this.asgardeo.getConfigData(); const baseUrl: string = configData?.baseUrl as string; return await getOrganization({ baseUrl, - organizationId, headers: { Authorization: `Bearer ${await this.getAccessToken(userId)}`, }, + organizationId, }); } catch (error) { throw new AsgardeoRuntimeError( @@ -301,7 +305,7 @@ class AsgardeoNextClient exte override async getMyOrganizations(options?: any, userId?: string): Promise { try { - const configData = await this.asgardeo.getConfigData(); + const configData: AuthClientConfig = await this.asgardeo.getConfigData(); const baseUrl: string = configData?.baseUrl as string; return await getMeOrganizations({ @@ -324,10 +328,10 @@ class AsgardeoNextClient exte override async getAllOrganizations(options?: any, userId?: string): Promise { try { - const configData = await this.asgardeo.getConfigData(); + const configData: AuthClientConfig = await this.asgardeo.getConfigData(); const baseUrl: string = configData?.baseUrl as string; - return getAllOrganizations({ + return await getAllOrganizations({ baseUrl, headers: { Authorization: `Bearer ${await this.getAccessToken(userId)}`, @@ -347,17 +351,14 @@ class AsgardeoNextClient exte const idToken: IdToken = await this.asgardeo.getDecodedIdToken(userId); return { - orgHandle: idToken?.org_handle as string, - name: idToken?.org_name as string, id: idToken?.org_id as string, + name: idToken?.org_name as string, + orgHandle: idToken?.org_handle as string, }; } override async switchOrganization(organization: Organization, userId?: string): Promise { try { - const configData = await this.asgardeo.getConfigData(); - const scopes = configData?.scopes; - if (!organization.id) { throw new AsgardeoRuntimeError( 'Organization ID is required for switching organizations', @@ -367,7 +368,7 @@ class AsgardeoNextClient exte ); } - const exchangeConfig = { + const exchangeConfig: TokenExchangeRequestConfig = { attachToken: false, data: { client_id: '{{clientId}}', @@ -393,6 +394,7 @@ class AsgardeoNextClient exte } } + // eslint-disable-next-line class-methods-use-this override isLoading(): boolean { return false; } @@ -409,9 +411,10 @@ class AsgardeoNextClient exte * Gets the access token from the session cookie if no sessionId is provided, * otherwise falls back to legacy client method. */ - async getAccessToken(sessionId?: string): Promise { + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars + async getAccessToken(_sessionId?: string): Promise { const {default: getAccessToken} = await import('./server/actions/getAccessToken'); - const token = await getAccessToken(); + const token: string | undefined = await getAccessToken(); if (typeof token !== 'string' || !token) { throw new AsgardeoRuntimeError( @@ -449,23 +452,23 @@ class AsgardeoNextClient exte onSignInSuccess?: (afterSignInUrl: string) => void, ): Promise; override async signIn(...args: any[]): Promise { - const arg1 = args[0]; - const arg2 = args[1]; - const arg3 = args[2]; - const arg4 = args[3]; + const arg1: any = args[0]; + const arg2: any = args[1]; + const arg3: any = args[2]; + const arg4: any = args[3]; if (typeof arg1 === 'object' && 'flowId' in arg1) { if (arg1.flowId === '') { const defaultSignInUrl: URL = new URL( await this.getAuthorizeRequestUrl({ - response_mode: 'direct', client_secret: '{{clientSecret}}', + response_mode: 'direct', }), ); return initializeEmbeddedSignInFlow({ - url: `${defaultSignInUrl.origin}${defaultSignInUrl.pathname}`, payload: Object.fromEntries(defaultSignInUrl.searchParams.entries()), + url: `${defaultSignInUrl.origin}${defaultSignInUrl.pathname}`, }); } @@ -513,11 +516,11 @@ class AsgardeoNextClient exte ); } - const firstArg = args[0]; + const firstArg: any = args[0]; if (typeof firstArg === 'object' && 'flowType' in firstArg) { - const configData = await this.asgardeo.getConfigData(); - const baseUrl = configData?.baseUrl; + const configData: AuthClientConfig = await this.asgardeo.getConfigData(); + const baseUrl: string | undefined = configData?.baseUrl; return executeEmbeddedSignUpFlow({ baseUrl, @@ -532,7 +535,8 @@ class AsgardeoNextClient exte ); } - override signInSilently(options?: SignInOptions): Promise { + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars + override signInSilently(_options?: SignInOptions): Promise { throw new AsgardeoRuntimeError( 'Not implemented', 'AsgardeoNextClient-signInSilently-NotImplementedError-001', @@ -568,6 +572,7 @@ class AsgardeoNextClient exte return this.asgardeo.getStorageManager(); } + // eslint-disable-next-line class-methods-use-this public async clearSession(): Promise { throw new AsgardeoRuntimeError( 'Not implemented', @@ -578,11 +583,11 @@ class AsgardeoNextClient exte } override async setSession(sessionData: Record, sessionId?: string): Promise { - return await (await this.asgardeo.getStorageManager()).setSessionData(sessionData, sessionId); + return (await this.asgardeo.getStorageManager()).setSessionData(sessionData, sessionId); } - override decodeJwtToken>(token: string): Promise { - return this.asgardeo.decodeJwtToken(token); + override decodeJwtToken>(token: string): Promise { + return this.asgardeo.decodeJwtToken(token); } } diff --git a/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx b/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx index 37e7f4fb7..51c136dd9 100644 --- a/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx +++ b/packages/nextjs/src/client/components/actions/SignInButton/SignInButton.tsx @@ -18,11 +18,12 @@ 'use client'; -import {forwardRef, ForwardRefExoticComponent, ReactElement, Ref, RefAttributes, useState, MouseEvent} from 'react'; import {AsgardeoRuntimeError} from '@asgardeo/node'; import {BaseSignInButton, BaseSignInButtonProps, useTranslation} from '@asgardeo/react'; -import useAsgardeo from '../../../../client/contexts/Asgardeo/useAsgardeo'; +import {AppRouterInstance} from 'next/dist/shared/lib/app-router-context.shared-runtime'; import {useRouter} from 'next/navigation'; +import {forwardRef, ForwardRefExoticComponent, ReactElement, Ref, RefAttributes, MouseEvent} from 'react'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; /** * Props interface of {@link SignInButton} @@ -58,26 +59,25 @@ export type SignInButtonProps = BaseSignInButtonProps & { * When using render props, the custom button should use `type="submit"` instead of `onClick={signIn}`. * The `signIn` function in render props is provided for API consistency but should not be used directly. */ -const SignInButton = forwardRef( +const SignInButton: ForwardRefExoticComponent> = forwardRef< + HTMLButtonElement, + SignInButtonProps +>( ( {className, style, children, preferences, onClick, signInOptions = {}, ...rest}: SignInButtonProps, ref: Ref, ): ReactElement => { const {signIn, signInUrl} = useAsgardeo(); - const router = useRouter(); + const router: AppRouterInstance = useRouter(); const {t} = useTranslation(preferences?.i18n); - const [isLoading, setIsLoading] = useState(false); - const handleOnClick = async (e: MouseEvent): Promise => { try { - setIsLoading(true); - // If a custom `signInUrl` is provided, use it for navigation. if (signInUrl) { router.push(signInUrl); - } else { - signIn && (await signIn(signInOptions)); + } else if (signIn) { + await signIn(signInOptions); } if (onClick) { @@ -90,8 +90,6 @@ const SignInButton = forwardRef( 'nextjs', 'Something went wrong while trying to sign in. Please try again later.', ); - } finally { - setIsLoading(false); } }; diff --git a/packages/nextjs/src/client/components/actions/SignOutButton/SignOutButton.tsx b/packages/nextjs/src/client/components/actions/SignOutButton/SignOutButton.tsx index f9c7bd108..6a7675ae5 100644 --- a/packages/nextjs/src/client/components/actions/SignOutButton/SignOutButton.tsx +++ b/packages/nextjs/src/client/components/actions/SignOutButton/SignOutButton.tsx @@ -18,10 +18,10 @@ 'use client'; -import {FC, forwardRef, ReactElement, Ref, useState, MouseEvent} from 'react'; import {BaseSignOutButton, BaseSignOutButtonProps, useTranslation} from '@asgardeo/react'; -import useAsgardeo from '../../../../client/contexts/Asgardeo/useAsgardeo'; +import {forwardRef, ForwardRefExoticComponent, ReactElement, Ref, RefAttributes, useState, MouseEvent} from 'react'; import logger from '../../../../utils/logger'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; /** * Interface for SignInButton component props. @@ -45,7 +45,10 @@ export type SignOutButtonProps = BaseSignOutButtonProps; * } * ``` */ -const SignOutButton = forwardRef( +const SignOutButton: ForwardRefExoticComponent> = forwardRef< + HTMLButtonElement, + SignOutButtonProps +>( ( {className, style, preferences, onClick, children, ...rest}: SignOutButtonProps, ref: Ref, diff --git a/packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx b/packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx index c921a3705..8f83422a1 100644 --- a/packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx +++ b/packages/nextjs/src/client/components/actions/SignUpButton/SignUpButton.tsx @@ -19,10 +19,11 @@ 'use client'; import {AsgardeoRuntimeError} from '@asgardeo/node'; -import {forwardRef, ForwardRefExoticComponent, MouseEvent, ReactElement, Ref, RefAttributes, useState} from 'react'; import {BaseSignUpButton, BaseSignUpButtonProps, useTranslation} from '@asgardeo/react'; -import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import {AppRouterInstance} from 'next/dist/shared/lib/app-router-context.shared-runtime'; import {useRouter} from 'next/navigation'; +import {forwardRef, ForwardRefExoticComponent, MouseEvent, ReactElement, Ref, RefAttributes, useState} from 'react'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; /** * Props interface of {@link SignUpButton} @@ -75,7 +76,7 @@ const SignUpButton: ForwardRefExoticComponent(({children, onClick, preferences, ...rest}: SignUpButtonProps, ref: Ref): ReactElement => { const {signUp, signUpUrl} = useAsgardeo(); - const router = useRouter(); + const router: AppRouterInstance = useRouter(); const {t} = useTranslation(preferences?.i18n); const [isLoading, setIsLoading] = useState(false); @@ -87,8 +88,8 @@ const SignUpButton: ForwardRefExoticComponent = ({children, fallback = null}): ReactElement => { +const Organization: FC = ({children, fallback = null}: OrganizationProps): ReactElement => { const {currentOrganization} = useOrganization(); return ( diff --git a/packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx b/packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx index ff0288597..9fe78b612 100644 --- a/packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx +++ b/packages/nextjs/src/client/components/presentation/OrganizationList/OrganizationList.tsx @@ -18,14 +18,14 @@ 'use client'; -import {AllOrganizationsApiResponse, withVendorCSSClassPrefix} from '@asgardeo/node'; -import {FC, ReactElement, useEffect, useMemo, CSSProperties, useState} from 'react'; +import {AllOrganizationsApiResponse} from '@asgardeo/node'; import { BaseOrganizationListProps, BaseOrganizationList, useOrganization, OrganizationWithSwitchAccess, } from '@asgardeo/react'; +import {FC, ReactElement, useEffect, useState} from 'react'; /** * Configuration options for the OrganizationList component. @@ -106,11 +106,7 @@ export interface OrganizationListProps * ``` */ export const OrganizationList: FC = ({ - autoFetch = true, - filter = '', - limit = 10, onOrganizationSelect, - recursive = false, ...baseProps }: OrganizationListProps): ReactElement => { const {getAllOrganizations, error, isLoading, myOrganizations} = useOrganization(); @@ -120,7 +116,7 @@ export const OrganizationList: FC = ({ }); useEffect(() => { - (async () => { + (async (): Promise => { setAllOrganizations(await getAllOrganizations()); })(); }, []); diff --git a/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx b/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx index 8c4da4a69..ef441fe95 100644 --- a/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx +++ b/packages/nextjs/src/client/components/presentation/OrganizationProfile/OrganizationProfile.tsx @@ -18,12 +18,13 @@ 'use client'; -import {FC, ReactElement, useEffect, useState} from 'react'; +import {OrganizationDetails, updateOrganization, createPatchOperations} from '@asgardeo/node'; import {BaseOrganizationProfile, BaseOrganizationProfileProps, useTranslation} from '@asgardeo/react'; -import {OrganizationDetails, getOrganization, updateOrganization, createPatchOperations} from '@asgardeo/node'; -import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import {FC, ReactElement, useEffect, useState} from 'react'; import getOrganizationAction from '../../../../server/actions/getOrganizationAction'; import getSessionId from '../../../../server/actions/getSessionId'; +import logger from '../../../../utils/logger'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; /** * Props for the OrganizationProfile component. @@ -138,17 +139,15 @@ const OrganizationProfile: FC = ({ onOpenChange, onUpdate, popupTitle, - loadingFallback =
Loading organization...
, - errorFallback =
Failed to load organization data
, ...rest }: OrganizationProfileProps): ReactElement => { const {baseUrl} = useAsgardeo(); const {t} = useTranslation(); const [organization, setOrganization] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); + const [, setLoading] = useState(true); + const [, setError] = useState(false); - const fetchOrganization = async () => { + const fetchOrganization = async (): Promise => { if (!baseUrl || !organizationId) { setLoading(false); setError(true); @@ -158,7 +157,8 @@ const OrganizationProfile: FC = ({ try { setLoading(true); setError(false); - const result = await getOrganizationAction(organizationId, (await getSessionId()) as string); + const result: {data?: {organization?: OrganizationDetails}; error: string | null; success: boolean} = + await getOrganizationAction(organizationId, (await getSessionId()) as string); if (result.data?.organization) { setOrganization(result.data.organization); @@ -168,7 +168,7 @@ const OrganizationProfile: FC = ({ setError(true); } catch (err) { - console.error('Failed to fetch organization:', err); + logger.error('Failed to fetch organization:', err); setError(true); setOrganization(null); } finally { @@ -185,12 +185,13 @@ const OrganizationProfile: FC = ({ try { // Convert payload to patch operations format - const operations = createPatchOperations(payload); + const operations: Array<{operation: 'REPLACE' | 'REMOVE'; path: string; value?: any}> = + createPatchOperations(payload); await updateOrganization({ baseUrl, - organizationId, operations, + organizationId, }); // Refetch organization data after update await fetchOrganization(); @@ -200,7 +201,7 @@ const OrganizationProfile: FC = ({ await onUpdate(payload); } } catch (err) { - console.error('Failed to update organization:', err); + logger.error('Failed to update organization:', err); throw err; } }; diff --git a/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx index 22945cf77..19ee2a8fd 100644 --- a/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx +++ b/packages/nextjs/src/client/components/presentation/OrganizationSwitcher/OrganizationSwitcher.tsx @@ -18,7 +18,7 @@ 'use client'; -import {FC, ReactElement, useState} from 'react'; +import {Organization} from '@asgardeo/node'; import { BaseOrganizationSwitcher, BaseOrganizationSwitcherProps, @@ -26,11 +26,11 @@ import { useOrganization, useTranslation, } from '@asgardeo/react'; +import {FC, ReactElement, useState} from 'react'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; -import {CreateOrganization} from '../CreateOrganization/CreateOrganization'; -import OrganizationProfile from '../OrganizationProfile/OrganizationProfile'; -import OrganizationList from '../OrganizationList/OrganizationList'; -import {Organization} from '@asgardeo/node'; +import {CreateOrganization} from '../CreateOrganization/CreateOrganization.js'; +import OrganizationList from '../OrganizationList/OrganizationList.js'; +import OrganizationProfile from '../OrganizationProfile/OrganizationProfile.js'; /** * Props interface for the OrganizationSwitcher component. @@ -143,7 +143,8 @@ export const OrganizationSwitcher: FC = ({ onClick: (): void => setIsCreateOrgOpen(true), }); - const menuItems = props.menuItems ? [...defaultMenuItems, ...props.menuItems] : defaultMenuItems; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const menuItems: any[] = props['menuItems'] ? [...defaultMenuItems, ...props['menuItems']] : defaultMenuItems; return ( <> diff --git a/packages/nextjs/src/client/components/presentation/SignUp/SignUp.tsx b/packages/nextjs/src/client/components/presentation/SignUp/SignUp.tsx index 6ea895e67..900d76c04 100644 --- a/packages/nextjs/src/client/components/presentation/SignUp/SignUp.tsx +++ b/packages/nextjs/src/client/components/presentation/SignUp/SignUp.tsx @@ -22,11 +22,10 @@ import { AsgardeoRuntimeError, EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteResponse, - EmbeddedFlowResponseType, EmbeddedFlowType, } from '@asgardeo/node'; -import {FC} from 'react'; import {BaseSignUp, BaseSignUpProps} from '@asgardeo/react'; +import {FC} from 'react'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; /** @@ -64,8 +63,14 @@ export type SignUpProps = BaseSignUpProps; * }; * ``` */ -const SignUp: FC = ({className, size = 'medium', variant = 'outlined', afterSignUpUrl, onError}) => { - const {signUp, isInitialized} = useAsgardeo(); +const SignUp: FC = ({ + className, + size = 'medium', + variant = 'outlined', + afterSignUpUrl, + onError, +}: SignUpProps) => { + const {signUp} = useAsgardeo(); /** * Initialize the sign-up flow. diff --git a/packages/nextjs/src/client/components/presentation/User/User.tsx b/packages/nextjs/src/client/components/presentation/User/User.tsx index 6cf743df6..269a802b6 100644 --- a/packages/nextjs/src/client/components/presentation/User/User.tsx +++ b/packages/nextjs/src/client/components/presentation/User/User.tsx @@ -18,9 +18,9 @@ 'use client'; +import {BaseUser, BaseUserProps} from '@asgardeo/react'; import {FC, ReactElement, ReactNode} from 'react'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; -import {BaseUser, BaseUserProps} from '@asgardeo/react'; /** * Props for the User component. @@ -64,7 +64,7 @@ export interface UserProps extends Omit { * } * ``` */ -const User: FC = ({children, fallback = null}): ReactElement => { +const User: FC = ({children, fallback = null}: UserProps): ReactElement => { const {user} = useAsgardeo(); return ( diff --git a/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx b/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx index 44045c810..6672c1dd9 100644 --- a/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx +++ b/packages/nextjs/src/client/components/presentation/UserDropdown/UserDropdown.tsx @@ -18,10 +18,10 @@ 'use client'; -import {FC, ReactElement, ReactNode, useState} from 'react'; import {BaseUserDropdown, BaseUserDropdownProps} from '@asgardeo/react'; +import {FC, ReactElement, ReactNode, useState} from 'react'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; -import UserProfile from '../UserProfile/UserProfile'; +import UserProfile from '../UserProfile/UserProfile.js'; /** * Render props data passed to the children function @@ -119,27 +119,29 @@ const UserDropdown: FC = ({ const {user, isLoading, signOut} = useAsgardeo(); const [isProfileOpen, setIsProfileOpen] = useState(false); - const handleManageProfile = () => { + const handleManageProfile = (): void => { setIsProfileOpen(true); }; - const handleSignOut = () => { + const handleSignOut = (): void => { signOut(); - onSignOut && onSignOut(); + if (onSignOut) { + onSignOut(); + } }; - const closeProfile = () => { + const closeProfile = (): void => { setIsProfileOpen(false); }; // Prepare render props data const renderProps: UserDropdownRenderProps = { - user, + closeProfile, isLoading: isLoading as boolean, + isProfileOpen, openProfile: handleManageProfile, signOut: handleSignOut, - isProfileOpen, - closeProfile, + user, }; // If children render prop is provided, use it for complete customization diff --git a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx index 6465b4e73..bfd86ea0e 100644 --- a/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx +++ b/packages/nextjs/src/client/components/presentation/UserProfile/UserProfile.tsx @@ -18,12 +18,10 @@ 'use client'; -import {FC, ReactElement} from 'react'; +import {Schema, User} from '@asgardeo/node'; import {BaseUserProfile, BaseUserProfileProps, useUser} from '@asgardeo/react'; -import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +import {FC, ReactElement} from 'react'; import getSessionId from '../../../../server/actions/getSessionId'; -import updateUserProfileAction from '../../../../server/actions/updateUserProfileAction'; -import {Schema, User} from '@asgardeo/node'; /** * Props for the UserProfile component. @@ -55,11 +53,13 @@ export type UserProfileProps = Omit = ({...rest}: UserProfileProps): ReactElement => { - const {baseUrl} = useAsgardeo(); const {profile, flattenedProfile, schemas, onUpdateProfile, updateProfile} = useUser(); const handleProfileUpdate = async (payload: any): Promise => { - const result = await updateProfile(payload, (await getSessionId()) as string); + const result: {data: {user: User}; error: string; success: boolean} = await updateProfile( + payload, + (await getSessionId()) as string, + ); onUpdateProfile(result?.data?.user); }; diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts index 56a756cec..e42d6408c 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoContext.ts @@ -19,7 +19,6 @@ 'use client'; import {AsgardeoContextProps as AsgardeoReactContextProps} from '@asgardeo/react'; -import {EmbeddedFlowExecuteRequestConfig, EmbeddedSignInFlowHandleRequestPayload, User} from '@asgardeo/node'; import {Context, createContext} from 'react'; /** @@ -31,18 +30,18 @@ export type AsgardeoContextProps = Partial; * Context object for managing the Authentication flow builder core context. */ const AsgardeoContext: Context = createContext({ - organizationHandle: undefined, - applicationId: undefined, - signInUrl: undefined, - signUpUrl: undefined, afterSignInUrl: undefined, + applicationId: undefined, baseUrl: undefined, isInitialized: false, isLoading: true, isSignedIn: false, + organizationHandle: undefined, signIn: () => Promise.resolve({} as any), + signInUrl: undefined, signOut: () => Promise.resolve({} as any), signUp: () => Promise.resolve({} as any), + signUpUrl: undefined, user: null, }); diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx index 4b52db7d0..475192363 100644 --- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx @@ -43,6 +43,8 @@ import { BrandingProvider, getActiveTheme, } from '@asgardeo/react'; +import {ReadonlyURLSearchParams} from 'next/dist/client/components/navigation.react-server'; +import {AppRouterInstance} from 'next/dist/shared/lib/app-router-context.shared-runtime'; import {useRouter, useSearchParams} from 'next/navigation'; import {FC, PropsWithChildren, RefObject, useEffect, useMemo, useRef, useState} from 'react'; import AsgardeoContext, {AsgardeoContextProps} from './AsgardeoContext'; @@ -104,8 +106,8 @@ const AsgardeoClientProvider: FC> brandingPreference, }: PropsWithChildren) => { const reRenderCheckRef: RefObject = useRef(false); - const router = useRouter(); - const searchParams = useSearchParams(); + const router: AppRouterInstance = useRouter(); + const searchParams: ReadonlyURLSearchParams = useSearchParams(); const [isLoading, setIsLoading] = useState(true); const [user, setUser] = useState(_user); const [userProfile, setUserProfile] = useState(_userProfile); @@ -135,13 +137,12 @@ const AsgardeoClientProvider: FC> // Don't handle callback if already signed in if (isSignedIn) return; - (async () => { + (async (): Promise => { try { - const code = searchParams.get('code'); - const state = searchParams.get('state'); - const sessionState = searchParams.get('session_state'); - const error = searchParams.get('error'); - const errorDescription = searchParams.get('error_description'); + const code: string | null = searchParams.get('code'); + const state: string | null = searchParams.get('state'); + const sessionState: string | null = searchParams.get('session_state'); + const error: string | null = searchParams.get('error'); // Check for OAuth errors first if (error) { @@ -154,7 +155,11 @@ const AsgardeoClientProvider: FC> if (code && state) { setIsLoading(true); - const result = await handleOAuthCallback(code, state, sessionState || undefined); + const result: {error?: string; redirectUrl?: string; success: boolean} = await handleOAuthCallback( + code, + state, + sessionState || undefined, + ); if (result.success) { // Redirect to the success URL @@ -176,13 +181,6 @@ const AsgardeoClientProvider: FC> })(); }, []); - const isDarkMode: boolean = useMemo(() => { - if (!preferences?.theme?.mode || preferences.theme.mode === 'system') { - return window.matchMedia('(prefers-color-scheme: dark)').matches; - } - return preferences.theme.mode === 'dark'; - }, [preferences?.theme?.mode]); - useEffect(() => { // Set loading to false when server has resolved authentication state setIsLoading(false); @@ -191,7 +189,7 @@ const AsgardeoClientProvider: FC> const handleSignIn = async ( payload: EmbeddedSignInFlowHandleRequestPayload, request: EmbeddedFlowExecuteRequestConfig, - ) => { + ): Promise => { if (!signIn) { throw new AsgardeoRuntimeError( '`signIn` function is not available.', @@ -200,37 +198,33 @@ const AsgardeoClientProvider: FC> ); } - try { - const result = await signIn(payload, request); + const result: any = await signIn(payload, request); - // Redirect based flow URL is sent as `signInUrl` in the response. - if (result?.data?.signInUrl) { - router.push(result.data.signInUrl); - - return; - } + // Redirect based flow URL is sent as `signInUrl` in the response. + if (result?.data?.signInUrl) { + router.push(result.data.signInUrl); - // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignInUrl` in the response. - if (result?.data?.afterSignInUrl) { - router.push(result.data.afterSignInUrl); + return undefined; + } - return; - } + // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignInUrl` in the response. + if (result?.data?.afterSignInUrl) { + router.push(result.data.afterSignInUrl); - if (result?.error) { - throw new Error(result.error); - } + return undefined; + } - return result?.data ?? result; - } catch (error) { - throw error; + if (result?.error) { + throw new Error(result.error); } + + return result?.data ?? result; }; const handleSignUp = async ( payload: EmbeddedFlowExecuteRequestPayload, request: EmbeddedFlowExecuteRequestConfig, - ) => { + ): Promise => { if (!signUp) { throw new AsgardeoRuntimeError( '`signUp` function is not available.', @@ -239,45 +233,41 @@ const AsgardeoClientProvider: FC> ); } - try { - const result = await signUp(payload, request); + const result: any = await signUp(payload, request); - // Redirect based flow URL is sent as `signUpUrl` in the response. - if (result?.data?.signUpUrl) { - router.push(result.data.signUpUrl); + // Redirect based flow URL is sent as `signUpUrl` in the response. + if (result?.data?.signUpUrl) { + router.push(result.data.signUpUrl); - return; - } - - // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignUpUrl` in the response. - if (result?.data?.afterSignUpUrl) { - router.push(result.data.afterSignUpUrl); + return undefined; + } - return; - } + // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignUpUrl` in the response. + if (result?.data?.afterSignUpUrl) { + router.push(result.data.afterSignUpUrl); - if (result?.error) { - throw new Error(result.error); - } + return undefined; + } - return result?.data ?? result; - } catch (error) { - throw error; + if (result?.error) { + throw new Error(result.error); } + + return result?.data ?? result; }; - const handleSignOut = async () => { + const handleSignOut = async (): Promise => { logger.debug('[AsgardeoClientProvider][handleSignOut] `handleSignOut` called.'); try { - const result = await signOut(); + const result: any = await signOut(); logger.debug('[AsgardeoClientProvider][handleSignOut] Sign out result:', result); if (result?.data?.afterSignOutUrl) { router.push(result.data.afterSignOutUrl); - return {redirected: true, location: result.data.afterSignOutUrl}; + return {location: result.data.afterSignOutUrl, redirected: true}; } if (result?.error) { @@ -293,32 +283,34 @@ const AsgardeoClientProvider: FC> '[AsgardeoClientProvider][handleSignOut] Error occurred during signing the user out with a button click:', error, ); + + return undefined; } }; - const contextValue = useMemo( + const contextValue: AsgardeoContextProps = useMemo( () => ({ + applicationId, baseUrl, - user, - isSignedIn, isLoading, + isSignedIn, + organizationHandle, signIn: handleSignIn, + signInUrl, signOut: handleSignOut, signUp: handleSignUp, - signInUrl, signUpUrl, - applicationId, - organizationHandle, + user, }), [baseUrl, user, isSignedIn, isLoading, signInUrl, signUpUrl, applicationId, organizationHandle], ); const handleProfileUpdate = (payload: User): void => { setUser(payload); - setUserProfile(prev => ({ + setUserProfile((prev: UserProfile) => ({ ...prev, - profile: payload, flattenedProfile: generateFlattenedUserProfile(payload, prev?.schemas), + profile: payload, })); }; diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 859dfe655..1320a994a 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -17,7 +17,6 @@ */ export {default as useAsgardeo} from './contexts/Asgardeo/useAsgardeo'; -export * from './contexts/Asgardeo/useAsgardeo'; export {default as Organization} from './components/presentation/Organization/Organization'; export {OrganizationProps} from './components/presentation/Organization/Organization'; diff --git a/packages/nextjs/src/configs/InternalAuthAPIRoutesConfig.ts b/packages/nextjs/src/configs/InternalAuthAPIRoutesConfig.ts index a0dd32cbd..3f3e01372 100644 --- a/packages/nextjs/src/configs/InternalAuthAPIRoutesConfig.ts +++ b/packages/nextjs/src/configs/InternalAuthAPIRoutesConfig.ts @@ -19,11 +19,11 @@ import {InternalAuthAPIRoutes} from '../models/api'; const InternalAuthAPIRoutesConfig: InternalAuthAPIRoutes = { - user: '/api/auth/user', session: '/api/auth/session', signIn: '/api/auth/signin', signOut: '/api/auth/signout', - signUp: undefined + signUp: undefined, + user: '/api/auth/user', }; export default InternalAuthAPIRoutesConfig; diff --git a/packages/nextjs/src/models/api.ts b/packages/nextjs/src/models/api.ts index c9d5ae7ee..464201f20 100644 --- a/packages/nextjs/src/models/api.ts +++ b/packages/nextjs/src/models/api.ts @@ -21,17 +21,11 @@ * These routes are used internally by the Asgardeo Next.js SDK for handling authentication flows. */ export interface InternalAuthAPIRoutes { - /** - * Route for handling user information retrieval. - * This route should return the current user's information, such as username, email, etc. - */ - user: string; /** * Route for handling session management. * This route should return the current signed-in status. */ session: string; - /** * Route for handling sign-in requests. * This route should handle the sign-in flow and redirect users to the appropriate authentication endpoint. @@ -49,4 +43,10 @@ export interface InternalAuthAPIRoutes { * This route should handle the sign-up flow and redirect users to the appropriate registration endpoint. */ signUp?: string; + + /** + * Route for handling user information retrieval. + * This route should return the current user's information, such as username, email, etc. + */ + user: string; } diff --git a/packages/nextjs/src/server/AsgardeoProvider.tsx b/packages/nextjs/src/server/AsgardeoProvider.tsx index b053fa723..d4760f7b4 100644 --- a/packages/nextjs/src/server/AsgardeoProvider.tsx +++ b/packages/nextjs/src/server/AsgardeoProvider.tsx @@ -18,7 +18,7 @@ 'use server'; -import {BrandingPreference, AsgardeoRuntimeError, Organization, User, UserProfile} from '@asgardeo/node'; +import {BrandingPreference, AsgardeoRuntimeError, IdToken, Organization, User, UserProfile} from '@asgardeo/node'; import {AsgardeoProviderProps} from '@asgardeo/react'; import {FC, PropsWithChildren, ReactElement} from 'react'; import createOrganization from './actions/createOrganization'; @@ -38,9 +38,10 @@ import signUpAction from './actions/signUpAction'; import switchOrganization from './actions/switchOrganization'; import updateUserProfileAction from './actions/updateUserProfileAction'; import AsgardeoNextClient from '../AsgardeoNextClient'; -import AsgardeoClientProvider from '../client/contexts/Asgardeo/AsgardeoProvider'; +import AsgardeoClientProvider from '../client/contexts/Asgardeo/AsgardeoProvider.js'; import {AsgardeoNextConfig} from '../models/config'; import logger from '../utils/logger'; +import {SessionTokenPayload} from '../utils/SessionManager'; /** * Props interface of {@link AsgardeoServerProvider} @@ -71,7 +72,7 @@ const AsgardeoServerProvider: FC> afterSignOutUrl, ..._config }: PropsWithChildren): Promise => { - const asgardeoClient = AsgardeoNextClient.getInstance(); + const asgardeoClient: AsgardeoNextClient = AsgardeoNextClient.getInstance(); let config: Partial = {}; try { @@ -96,15 +97,15 @@ const AsgardeoServerProvider: FC> } // Try to get session information from JWT first, then fall back to legacy - const sessionPayload = await getSessionPayload(); + const sessionPayload: SessionTokenPayload | undefined = await getSessionPayload(); const sessionId: string = sessionPayload?.sessionId || (await getSessionId()) || ''; - const _isSignedIn: boolean = sessionPayload ? true : await isSignedIn(sessionId); + const signedIn: boolean = sessionPayload ? true : await isSignedIn(sessionId); let user: User = {}; let userProfile: UserProfile = { - schemas: [], - profile: {}, flattenedProfile: {}, + profile: {}, + schemas: [], }; let currentOrganization: Organization = { id: '', @@ -114,15 +115,15 @@ const AsgardeoServerProvider: FC> let myOrganizations: Organization[] = []; let brandingPreference: BrandingPreference | null = null; - if (_isSignedIn) { - let updatedBaseUrl = config?.baseUrl; + if (signedIn) { + let updatedBaseUrl: string | undefined = config?.baseUrl; if (sessionPayload?.organizationId) { updatedBaseUrl = `${config?.baseUrl}/o`; config = {...config, baseUrl: updatedBaseUrl}; } else if (sessionId) { try { - const idToken = await asgardeoClient.getDecodedIdToken(sessionId); + const idToken: IdToken = await asgardeoClient.getDecodedIdToken(sessionId); if (idToken?.['user_org']) { updatedBaseUrl = `${config?.baseUrl}/o`; config = {...config, baseUrl: updatedBaseUrl}; @@ -133,22 +134,35 @@ const AsgardeoServerProvider: FC> } try { - const userResponse = await getUserAction(sessionId); - const userProfileResponse = await getUserProfileAction(sessionId); - const currentOrganizationResponse = await getCurrentOrganizationAction(sessionId); + const userResponse: { + data: {user: User | null}; + error: string | null; + success: boolean; + } = await getUserAction(sessionId); + const userProfileResponse: { + data: {userProfile: UserProfile}; + error: string | null; + success: boolean; + } = await getUserProfileAction(sessionId); + const currentOrganizationResponse: { + data: {organization?: Organization; user?: Record}; + error: string | null; + success: boolean; + } = await getCurrentOrganizationAction(sessionId); if (sessionId) { myOrganizations = await getMyOrganizations({}, sessionId); } else { + // eslint-disable-next-line no-console console.warn('[AsgardeoServerProvider] No session ID available, skipping organization fetch'); } user = userResponse.data?.user || {}; - userProfile = userProfileResponse.data?.userProfile; + userProfile = userProfileResponse.data?.userProfile ?? userProfile; currentOrganization = currentOrganizationResponse?.data?.organization as Organization; } catch (error) { user = {}; - userProfile = {schemas: [], profile: {}, flattenedProfile: {}}; + userProfile = {flattenedProfile: {}, profile: {}, schemas: []}; currentOrganization = {id: '', name: '', orgHandle: ''}; myOrganizations = []; } @@ -167,6 +181,7 @@ const AsgardeoServerProvider: FC> sessionId, ); } catch (error) { + // eslint-disable-next-line no-console console.warn('[AsgardeoServerProvider] Failed to fetch branding preference:', error); } } @@ -188,7 +203,7 @@ const AsgardeoServerProvider: FC> currentOrganization={currentOrganization} userProfile={userProfile} updateProfile={updateUserProfileAction} - isSignedIn={_isSignedIn} + isSignedIn={signedIn} myOrganizations={myOrganizations} getAllOrganizations={getAllOrganizations} switchOrganization={switchOrganization} diff --git a/packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts b/packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts index baa5837d4..6367ce537 100644 --- a/packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts +++ b/packages/nextjs/src/server/actions/__tests__/createOrganization.test.ts @@ -16,41 +16,41 @@ * under the License. */ -import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; +import {AsgardeoAPIError, Organization, CreateOrganizationPayload} from '@asgardeo/node'; +import {describe, it, expect, vi, beforeEach, afterEach, Mock} from 'vitest'; // Adjust these paths if your project structure is different +import AsgardeoNextClient from '../../../AsgardeoNextClient'; import createOrganization from '../createOrganization'; // Use the same class so we can assert instanceof and status code propagation -import { AsgardeoAPIError, Organization, CreateOrganizationPayload } from '@asgardeo/node'; + +// Pull the mocked modules so we can access their spies +import getSessionId from '../getSessionId'; // ---- Mocks ---- -vi.mock('../../../AsgardeoNextClient', () => { +vi.mock('../../../AsgardeoNextClient', () => // We return a default export with a static getInstance function we can stub - return { + ({ default: { getInstance: vi.fn(), }, - }; -}); + }), +); vi.mock('../getSessionId', () => ({ default: vi.fn(), })); -// Pull the mocked modules so we can access their spies -import AsgardeoNextClient from '../../../AsgardeoNextClient'; -import getSessionId from '../getSessionId'; - describe('createOrganization (Next.js server action)', () => { - const mockClient = { + const mockClient: {createOrganization: ReturnType} = { createOrganization: vi.fn(), }; const basePayload: CreateOrganizationPayload = { + description: 'Screen sharing organization', name: 'Team Viewer', orgHandle: 'team-viewer', - description: 'Screen sharing organization', parentId: 'parent-123', type: 'TENANT', }; @@ -77,7 +77,7 @@ describe('createOrganization (Next.js server action)', () => { it('should create an organization successfully when a sessionId is provided', async () => { mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - const result = await createOrganization(basePayload, 'sess-123'); + const result: Organization = await createOrganization(basePayload, 'sess-123'); expect(AsgardeoNextClient.getInstance).toHaveBeenCalledTimes(1); expect(getSessionId).not.toHaveBeenCalled(); @@ -88,7 +88,7 @@ describe('createOrganization (Next.js server action)', () => { it('should fall back to getSessionId when sessionId is undefined', async () => { mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - const result = await createOrganization(basePayload, undefined as unknown as string); + const result: Organization = await createOrganization(basePayload, undefined as unknown as string); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.createOrganization).toHaveBeenCalledWith(basePayload, 'sess-abc'); @@ -98,7 +98,7 @@ describe('createOrganization (Next.js server action)', () => { it('should fall back to getSessionId when sessionId is null', async () => { mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - const result = await createOrganization(basePayload, null as unknown as string); + const result: Organization = await createOrganization(basePayload, null as unknown as string); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.createOrganization).toHaveBeenCalledWith(basePayload, 'sess-abc'); @@ -108,7 +108,7 @@ describe('createOrganization (Next.js server action)', () => { it('should not call getSessionId when an empty string is passed (empty string is not nullish)', async () => { mockClient.createOrganization.mockResolvedValueOnce(mockOrg); - const result = await createOrganization(basePayload, ''); + const result: Organization = await createOrganization(basePayload, ''); expect(getSessionId).not.toHaveBeenCalled(); expect(mockClient.createOrganization).toHaveBeenCalledWith(basePayload, ''); @@ -116,18 +116,18 @@ describe('createOrganization (Next.js server action)', () => { }); it('should wrap an AsgardeoAPIError thrown by client.createOrganization, preserving statusCode', async () => { - const original = new AsgardeoAPIError( + const original: AsgardeoAPIError = new AsgardeoAPIError( 'Upstream validation failed', 'ORG_CREATE_400', 'server', - 400 + 400, ); mockClient.createOrganization.mockRejectedValueOnce(original); await expect(createOrganization(basePayload, 'sess-1')).rejects.toMatchObject({ constructor: AsgardeoAPIError, - statusCode: 400, message: expect.stringContaining('Failed to create the organization: Upstream validation failed'), + statusCode: 400, }); }); }); diff --git a/packages/nextjs/src/server/actions/__tests__/getAccessToken.test.ts b/packages/nextjs/src/server/actions/__tests__/getAccessToken.test.ts index 1e977113d..e2d1e56d0 100644 --- a/packages/nextjs/src/server/actions/__tests__/getAccessToken.test.ts +++ b/packages/nextjs/src/server/actions/__tests__/getAccessToken.test.ts @@ -17,42 +17,38 @@ */ // src/server/actions/__tests__/getAccessToken.test.ts -import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; +import {cookies} from 'next/headers'; +import {describe, it, expect, vi, beforeEach, afterEach, Mock} from 'vitest'; // SUT +import SessionManager from '../../../utils/SessionManager'; import getAccessToken from '../getAccessToken'; -// ---- Mocks ---- -vi.mock('next/headers', () => { - return { - cookies: vi.fn(), - }; -}); +// Pull mocked modules so we can control them -vi.mock('../../../utils/SessionManager', () => { - return { - default: { - getSessionCookieName: vi.fn(), - verifySessionToken: vi.fn(), - }, - }; -}); +// ---- Mocks ---- +vi.mock('next/headers', () => ({ + cookies: vi.fn(), +})); -// Pull mocked modules so we can control them -import { cookies } from 'next/headers'; -import SessionManager from '../../../utils/SessionManager'; +vi.mock('../../../utils/SessionManager', () => ({ + default: { + getSessionCookieName: vi.fn(), + verifySessionToken: vi.fn(), + }, +})); // A tiny helper type for the cookie store the SUT expects -type CookieVal = { value: string }; -type CookieStore = { get: (name: string) => CookieVal | undefined }; +type CookieVal = {value: string}; +type CookieStore = {get: (name: string) => CookieVal | undefined}; describe('getAccessToken', () => { - const SESSION_COOKIE_NAME = 'app_session'; + const SESSION_COOKIE_NAME: string = 'app_session'; const makeCookieStore = (map: Record): CookieStore => ({ - get: (name: string) => { - const v = map[name]; - return typeof v === 'string' ? { value: v } : undefined; + get: (name: string): CookieVal | undefined => { + const v: string | undefined = map[name]; + return typeof v === 'string' ? {value: v} : undefined; }, }); @@ -77,12 +73,10 @@ describe('getAccessToken', () => { it('should return the access token when the session cookie exists and verification succeeds', async () => { // Arrange - (cookies as unknown as Mock).mockResolvedValue( - makeCookieStore({ [SESSION_COOKIE_NAME]: 'signed.jwt.token' }), - ); + (cookies as unknown as Mock).mockResolvedValue(makeCookieStore({[SESSION_COOKIE_NAME]: 'signed.jwt.token'})); // Act - const token = await getAccessToken(); + const token: string | undefined = await getAccessToken(); // Assert expect(SessionManager.getSessionCookieName).toHaveBeenCalledTimes(1); @@ -94,7 +88,7 @@ describe('getAccessToken', () => { it('should return undefined when the session cookie is missing', async () => { // Arrange: no cookie present (default makeCookieStore({})) // Act - const token = await getAccessToken(); + const token: string | undefined = await getAccessToken(); // Assert expect(SessionManager.getSessionCookieName).toHaveBeenCalledTimes(1); @@ -103,43 +97,33 @@ describe('getAccessToken', () => { }); it('should return undefined when the session cookie value is an empty string', async () => { - (cookies as unknown as Mock).mockResolvedValue( - makeCookieStore({ [SESSION_COOKIE_NAME]: '' }), - ); + (cookies as unknown as Mock).mockResolvedValue(makeCookieStore({[SESSION_COOKIE_NAME]: ''})); - const token = await getAccessToken(); + const token: string | undefined = await getAccessToken(); expect(SessionManager.verifySessionToken).not.toHaveBeenCalled(); expect(token).toBeUndefined(); }); it('should return undefined when verifySessionToken throws (invalid or expired session)', async () => { - (cookies as unknown as Mock).mockResolvedValue( - makeCookieStore({ [SESSION_COOKIE_NAME]: 'bad.token' }), - ); - (SessionManager.verifySessionToken as unknown as Mock).mockRejectedValue( - new Error('invalid signature'), - ); + (cookies as unknown as Mock).mockResolvedValue(makeCookieStore({[SESSION_COOKIE_NAME]: 'bad.token'})); + (SessionManager.verifySessionToken as unknown as Mock).mockRejectedValue(new Error('invalid signature')); - const token = await getAccessToken(); + const token: string | undefined = await getAccessToken(); expect(SessionManager.verifySessionToken).toHaveBeenCalledWith('bad.token'); expect(token).toBeUndefined(); }); it('should return undefined when verification succeeds but accessToken is missing', async () => { - (cookies as unknown as Mock).mockResolvedValue( - makeCookieStore({ [SESSION_COOKIE_NAME]: 'signed.jwt.token' }), - ); + (cookies as unknown as Mock).mockResolvedValue(makeCookieStore({[SESSION_COOKIE_NAME]: 'signed.jwt.token'})); (SessionManager.verifySessionToken as unknown as Mock).mockResolvedValue({ // no accessToken field sub: 'user@tenant', }); - const token = await getAccessToken(); + const token: string | undefined = await getAccessToken(); expect(token).toBeUndefined(); }); - - }); diff --git a/packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts b/packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts index 0bbd82f15..196248c09 100644 --- a/packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts +++ b/packages/nextjs/src/server/actions/__tests__/getAllOrganizations.test.ts @@ -17,7 +17,13 @@ */ // src/server/actions/__tests__/getAllOrganizations.test.ts -import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; +import {AsgardeoAPIError, AllOrganizationsApiResponse} from '@asgardeo/node'; +import {describe, it, expect, vi, beforeEach, afterEach, Mock} from 'vitest'; + +// --- Now import the SUT and mocked deps --- +import AsgardeoNextClient from '../../../AsgardeoNextClient'; +import getAllOrganizations from '../getAllOrganizations'; +import getSessionId from '../getSessionId'; // --- Mocks MUST be defined before importing the SUT --- vi.mock('../../../AsgardeoNextClient', () => ({ @@ -30,25 +36,23 @@ vi.mock('../getSessionId', () => ({ default: vi.fn(), })); -// --- Now import the SUT and mocked deps --- -import getAllOrganizations from '../getAllOrganizations'; -import AsgardeoNextClient from '../../../AsgardeoNextClient'; -import getSessionId from '../getSessionId'; -import { AsgardeoAPIError, AllOrganizationsApiResponse } from '@asgardeo/node'; - describe('getAllOrganizations (Next.js server action)', () => { - const mockClient = { + const mockClient: {getAllOrganizations: ReturnType} = { getAllOrganizations: vi.fn(), }; - const baseOptions = { limit: 50, cursor: 'cur-1', filter: 'type eq "TENANT"' }; + const baseOptions: {cursor: string; filter: string; limit: number} = { + cursor: 'cur-1', + filter: 'type eq "TENANT"', + limit: 50, + }; const mockResponse: AllOrganizationsApiResponse = { data: [ - { id: 'org-001', name: 'Alpha', orgHandle: 'alpha' }, - { id: 'org-002', name: 'Beta', orgHandle: 'beta' }, + {id: 'org-001', name: 'Alpha', orgHandle: 'alpha'}, + {id: 'org-002', name: 'Beta', orgHandle: 'beta'}, ], - meta: { totalResults: 2, startIndex: 1, itemsPerPage: 2 }, + meta: {itemsPerPage: 2, startIndex: 1, totalResults: 2}, } as unknown as AllOrganizationsApiResponse; beforeEach(() => { @@ -67,7 +71,7 @@ describe('getAllOrganizations (Next.js server action)', () => { it('returns organizations when a sessionId is provided (no getSessionId fallback)', async () => { mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - const result = await getAllOrganizations(baseOptions, 'sess-123'); + const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, 'sess-123'); expect(AsgardeoNextClient.getInstance).toHaveBeenCalledTimes(1); expect(getSessionId).not.toHaveBeenCalled(); @@ -78,7 +82,7 @@ describe('getAllOrganizations (Next.js server action)', () => { it('falls back to getSessionId when sessionId is undefined', async () => { mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - const result = await getAllOrganizations(baseOptions, undefined); + const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, undefined); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.getAllOrganizations).toHaveBeenCalledWith(baseOptions, 'sess-abc'); @@ -88,7 +92,7 @@ describe('getAllOrganizations (Next.js server action)', () => { it('falls back to getSessionId when sessionId is null', async () => { mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - const result = await getAllOrganizations(baseOptions, null as unknown as string); + const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, null as unknown as string); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.getAllOrganizations).toHaveBeenCalledWith(baseOptions, 'sess-abc'); @@ -98,7 +102,7 @@ describe('getAllOrganizations (Next.js server action)', () => { it('does not call getSessionId for an empty string sessionId (empty string is not nullish)', async () => { mockClient.getAllOrganizations.mockResolvedValueOnce(mockResponse); - const result = await getAllOrganizations(baseOptions, ''); + const result: AllOrganizationsApiResponse = await getAllOrganizations(baseOptions, ''); expect(getSessionId).not.toHaveBeenCalled(); expect(mockClient.getAllOrganizations).toHaveBeenCalledWith(baseOptions, ''); @@ -106,20 +110,13 @@ describe('getAllOrganizations (Next.js server action)', () => { }); it('wraps an AsgardeoAPIError thrown by client.getAllOrganizations, preserving statusCode', async () => { - const upstream = new AsgardeoAPIError( - 'Upstream failed', - 'ORG_LIST_500', - 'server', - 503, - ); + const upstream: AsgardeoAPIError = new AsgardeoAPIError('Upstream failed', 'ORG_LIST_500', 'server', 503); mockClient.getAllOrganizations.mockRejectedValueOnce(upstream); await expect(getAllOrganizations(baseOptions, 'sess-x')).rejects.toMatchObject({ constructor: AsgardeoAPIError, - statusCode: 503, message: expect.stringContaining('Failed to get all the organizations for the user: Upstream failed'), + statusCode: 503, }); }); - }); - diff --git a/packages/nextjs/src/server/actions/__tests__/getBrandingPreference.test.ts b/packages/nextjs/src/server/actions/__tests__/getBrandingPreference.test.ts index bff80c182..0e1fab00d 100644 --- a/packages/nextjs/src/server/actions/__tests__/getBrandingPreference.test.ts +++ b/packages/nextjs/src/server/actions/__tests__/getBrandingPreference.test.ts @@ -17,16 +17,23 @@ */ // src/server/actions/__tests__/getBrandingPreference.test.ts -import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest'; +import {AsgardeoAPIError, getBrandingPreference as baseGetBrandingPreference} from '@asgardeo/node'; +import {describe, it, expect, vi, beforeEach, afterEach, type Mock} from 'vitest'; + +// Now import SUT and mocked exports +import getBrandingPreference from '../getBrandingPreference'; // Mock the upstream module first. Keep all dependencies inside the factory. vi.mock('@asgardeo/node', () => { - const getBrandingPreference = vi.fn(); + const getBrandingPreferenceMock: ReturnType = vi.fn(); class MockAsgardeoAPIError extends Error { code?: string; + source?: string; + statusCode?: number; + constructor(message: string, code?: string, source?: string, statusCode?: number) { super(message); this.name = 'AsgardeoAPIError'; @@ -38,26 +45,19 @@ vi.mock('@asgardeo/node', () => { return { AsgardeoAPIError: MockAsgardeoAPIError, - getBrandingPreference, + getBrandingPreference: getBrandingPreferenceMock, }; }); -// Now import SUT and mocked exports -import getBrandingPreference from '../getBrandingPreference'; -import { - AsgardeoAPIError, - getBrandingPreference as baseGetBrandingPreference, -} from '@asgardeo/node'; - describe('getBrandingPreference (Next.js server action)', () => { type BrandingPreference = Awaited>; type Cfg = Parameters[0]; - const cfg: Cfg = { orgId: 'org-001', locale: 'en-US' } as unknown as Cfg; + const cfg: Cfg = {locale: 'en-US', orgId: 'org-001'} as unknown as Cfg; const mockPref: BrandingPreference = { - theme: { colors: { primary: '#0055aa' } }, logoUrl: 'https://cdn.example.com/logo.png', + theme: {colors: {primary: '#0055aa'}}, } as unknown as BrandingPreference; beforeEach(() => { @@ -70,38 +70,36 @@ describe('getBrandingPreference (Next.js server action)', () => { }); it('should return branding preferences when upstream succeeds', async () => { - const result = await getBrandingPreference(cfg, 'sess-123'); + const result: BrandingPreference = await getBrandingPreference(cfg, 'sess-123'); expect(baseGetBrandingPreference).toHaveBeenCalledTimes(1); expect(baseGetBrandingPreference).toHaveBeenCalledWith(cfg); // Ensure sessionId is not forwarded - const call = (baseGetBrandingPreference as unknown as Mock).mock.calls[0]; + const call: unknown[] = (baseGetBrandingPreference as unknown as Mock).mock.calls[0]; expect(call.length).toBe(1); expect(result).toBe(mockPref); }); it('should wrap an AsgardeoAPIError from upstream, preserving statusCode', async () => { - const upstream = new AsgardeoAPIError('Not found', 'BRAND_404', 'server', 404); + const upstream: AsgardeoAPIError = new AsgardeoAPIError('Not found', 'BRAND_404', 'server', 404); (baseGetBrandingPreference as unknown as Mock).mockRejectedValueOnce(upstream); await expect(getBrandingPreference(cfg)).rejects.toMatchObject({ constructor: AsgardeoAPIError, - statusCode: 404, message: expect.stringContaining('Failed to get branding preferences: Not found'), + statusCode: 404, }); }); it('should wrap a generic Error with undefined statusCode', async () => { - (baseGetBrandingPreference as unknown as Mock).mockRejectedValueOnce( - new Error('network down'), - ); + (baseGetBrandingPreference as unknown as Mock).mockRejectedValueOnce(new Error('network down')); await expect(getBrandingPreference(cfg)).rejects.toMatchObject({ constructor: AsgardeoAPIError, - statusCode: undefined, message: expect.stringContaining('Failed to get branding preferences: network down'), + statusCode: undefined, }); }); @@ -110,8 +108,8 @@ describe('getBrandingPreference (Next.js server action)', () => { await expect(getBrandingPreference(cfg)).rejects.toMatchObject({ constructor: AsgardeoAPIError, - statusCode: undefined, message: expect.stringContaining('Failed to get branding preferences: boom'), + statusCode: undefined, }); }); }); diff --git a/packages/nextjs/src/server/actions/__tests__/getClientOrigin.test.ts b/packages/nextjs/src/server/actions/__tests__/getClientOrigin.test.ts index 3f788d3fe..ddc18314b 100644 --- a/packages/nextjs/src/server/actions/__tests__/getClientOrigin.test.ts +++ b/packages/nextjs/src/server/actions/__tests__/getClientOrigin.test.ts @@ -17,25 +17,27 @@ */ // src/server/actions/__tests__/getClientOrigin.test.ts -import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest'; +import {headers} from 'next/headers'; +import {describe, it, expect, vi, beforeEach, afterEach, type Mock} from 'vitest'; -//Mock next/headers BEFORE importing the SUT +// Import SUT and mocked dep +import getClientOrigin from '../getClientOrigin'; + +// Mock next/headers BEFORE importing the SUT vi.mock('next/headers', () => ({ headers: vi.fn(), })); -//Import SUT and mocked dep -import getClientOrigin from '../getClientOrigin'; -import { headers } from 'next/headers'; - // Helper: build a Headers-like object. get() should be case-insensitive. -type HLike = { get: (name: string) => string | null }; +type HLike = {get: (name: string) => string | null}; const makeHeaders = (map: Record): HLike => { const normalized: Record = {}; - for (const [k, v] of Object.entries(map)) normalized[k.toLowerCase()] = v; + Object.entries(map).forEach(([k, val]: [string, string | null | undefined]) => { + normalized[k.toLowerCase()] = val; + }); return { - get: (name: string) => { - const v = normalized[name.toLowerCase()]; + get: (name: string): string | null => { + const v: string | null | undefined = normalized[name.toLowerCase()]; return v == null ? null : v; // emulate real Headers.get(): string | null }, }; @@ -53,11 +55,9 @@ describe('getClientOrigin', () => { }); it('should return https origin when x-forwarded-proto is https and host is present', async () => { - (headers as unknown as Mock).mockResolvedValue( - makeHeaders({ host: 'example.com', 'x-forwarded-proto': 'https' }), - ); + (headers as unknown as Mock).mockResolvedValue(makeHeaders({host: 'example.com', 'x-forwarded-proto': 'https'})); - const origin = await getClientOrigin(); + const origin: string = await getClientOrigin(); expect(headers).toHaveBeenCalledTimes(1); expect(origin).toBe('https://example.com'); @@ -65,21 +65,19 @@ describe('getClientOrigin', () => { it('should fall back to http when x-forwarded-proto is missing', async () => { (headers as unknown as Mock).mockResolvedValue( - makeHeaders({ host: 'svc.internal' /* x-forwarded-proto: missing */ }), + makeHeaders({host: 'svc.internal' /* x-forwarded-proto: missing */}), ); - const origin = await getClientOrigin(); + const origin: string = await getClientOrigin(); expect(origin).toBe('http://svc.internal'); }); it('should return "protocol://null" when host is missing', async () => { // host header absent -> get('host') returns null -> interpolates as "null" - (headers as unknown as Mock).mockResolvedValue( - makeHeaders({ 'x-forwarded-proto': 'https' }), - ); + (headers as unknown as Mock).mockResolvedValue(makeHeaders({'x-forwarded-proto': 'https'})); - const origin = await getClientOrigin(); + const origin: string = await getClientOrigin(); expect(origin).toBe('https://null'); }); @@ -90,4 +88,3 @@ describe('getClientOrigin', () => { await expect(getClientOrigin()).rejects.toThrow('headers not available'); }); }); - diff --git a/packages/nextjs/src/server/actions/__tests__/getCurrentOrganizationAction.test.ts b/packages/nextjs/src/server/actions/__tests__/getCurrentOrganizationAction.test.ts index 43bb924aa..8cc5e5511 100644 --- a/packages/nextjs/src/server/actions/__tests__/getCurrentOrganizationAction.test.ts +++ b/packages/nextjs/src/server/actions/__tests__/getCurrentOrganizationAction.test.ts @@ -17,7 +17,11 @@ */ // src/server/actions/__tests__/getCurrentOrganizationAction.test.ts -import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest'; +import {describe, it, expect, vi, beforeEach, afterEach, type Mock} from 'vitest'; + +// --- Import SUT and mocked deps --- +import AsgardeoNextClient from '../../../AsgardeoNextClient'; +import getCurrentOrganizationAction from '../getCurrentOrganizationAction'; // --- Mock client factory BEFORE importing SUT --- vi.mock('../../../AsgardeoNextClient', () => ({ @@ -26,20 +30,18 @@ vi.mock('../../../AsgardeoNextClient', () => ({ }, })); -// --- Import SUT and mocked deps --- -import getCurrentOrganizationAction from '../getCurrentOrganizationAction'; -import AsgardeoNextClient from '../../../AsgardeoNextClient'; - // A light org shape for testing (only fields we assert on) -type Org = { id: string; name: string; orgHandle?: string }; +type Org = {id: string; name: string; orgHandle?: string}; describe('getCurrentOrganizationAction', () => { - const mockClient = { + type ActionResult = Awaited>; + + const mockClient: {getCurrentOrganization: ReturnType} = { getCurrentOrganization: vi.fn(), }; - const sessionId = 'sess-123'; - const org: Org = { id: 'org-001', name: 'Alpha', orgHandle: 'alpha' }; + const sessionId: string = 'sess-123'; + const org: Org = {id: 'org-001', name: 'Alpha', orgHandle: 'alpha'}; beforeEach(() => { vi.resetAllMocks(); @@ -53,7 +55,7 @@ describe('getCurrentOrganizationAction', () => { it('returns success with organization when upstream succeeds', async () => { mockClient.getCurrentOrganization.mockResolvedValueOnce(org); - const result = await getCurrentOrganizationAction(sessionId); + const result: ActionResult = await getCurrentOrganizationAction(sessionId); expect(AsgardeoNextClient.getInstance).toHaveBeenCalledTimes(1); expect(mockClient.getCurrentOrganization).toHaveBeenCalledWith(sessionId); @@ -66,7 +68,7 @@ describe('getCurrentOrganizationAction', () => { it('should pass through the provided sessionId even if it is an empty string', async () => { mockClient.getCurrentOrganization.mockResolvedValueOnce(org); - const result = await getCurrentOrganizationAction(''); + const result: ActionResult = await getCurrentOrganizationAction(''); expect(mockClient.getCurrentOrganization).toHaveBeenCalledWith(''); expect(result.success).toBe(true); @@ -76,12 +78,12 @@ describe('getCurrentOrganizationAction', () => { it('should return failure shape when client.getCurrentOrganization rejects', async () => { mockClient.getCurrentOrganization.mockRejectedValueOnce(new Error('upstream down')); - const result = await getCurrentOrganizationAction(sessionId); + const result: ActionResult = await getCurrentOrganizationAction(sessionId); expect(result.success).toBe(false); expect(result.error).toBe('Failed to get the current organization'); // Matches the function’s failure payload shape - expect(result.data).toEqual({ user: {} }); + expect(result.data).toEqual({user: {}}); }); it('should return failure shape when AsgardeoNextClient.getInstance throws', async () => { @@ -89,18 +91,18 @@ describe('getCurrentOrganizationAction', () => { throw new Error('factory failed'); }); - const result = await getCurrentOrganizationAction(sessionId); + const result: ActionResult = await getCurrentOrganizationAction(sessionId); expect(result.success).toBe(false); expect(result.error).toBe('Failed to get the current organization'); - expect(result.data).toEqual({ user: {} }); + expect(result.data).toEqual({user: {}}); }); it('should not mutate the organization object returned by upstream', async () => { - const upstreamOrg = { ...org, extra: { nested: true } }; + const upstreamOrg: Org & {extra: {nested: boolean}} = {...org, extra: {nested: true}}; mockClient.getCurrentOrganization.mockResolvedValueOnce(upstreamOrg); - const result = await getCurrentOrganizationAction(sessionId); + const result: ActionResult = await getCurrentOrganizationAction(sessionId); // exact deep equality: whatever upstream returns is passed through expect(result.data.organization).toEqual(upstreamOrg); diff --git a/packages/nextjs/src/server/actions/__tests__/getMyOrganizations.test.ts b/packages/nextjs/src/server/actions/__tests__/getMyOrganizations.test.ts index 199b9e193..1027a9d25 100644 --- a/packages/nextjs/src/server/actions/__tests__/getMyOrganizations.test.ts +++ b/packages/nextjs/src/server/actions/__tests__/getMyOrganizations.test.ts @@ -17,7 +17,13 @@ */ // src/server/actions/__tests__/getMyOrganizations.test.ts -import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest'; +import {AsgardeoAPIError, Organization} from '@asgardeo/node'; +import {describe, it, expect, vi, beforeEach, afterEach, type Mock} from 'vitest'; + +// --- Import SUT and mocked deps --- +import AsgardeoNextClient from '../../../AsgardeoNextClient'; +import getMyOrganizations from '../getMyOrganizations'; +import getSessionId from '../getSessionId'; // --- Mocks (declare BEFORE importing the SUT) --- vi.mock('../../../AsgardeoNextClient', () => ({ @@ -31,22 +37,16 @@ vi.mock('../getSessionId', () => ({ default: vi.fn(), })); -// --- Import SUT and mocked deps --- -import getMyOrganizations from '../getMyOrganizations'; -import AsgardeoNextClient from '../../../AsgardeoNextClient'; -import getSessionId from '../getSessionId'; -import { AsgardeoAPIError } from '@asgardeo/node'; - describe('getMyOrganizations (Next.js server action)', () => { - const mockClient = { + const mockClient: {getAccessToken: ReturnType; getMyOrganizations: ReturnType} = { getAccessToken: vi.fn(), getMyOrganizations: vi.fn(), }; - const options = { limit: 25, filter: 'type eq "TENANT"' }; - const orgs = [ - { id: 'org-1', name: 'Alpha', orgHandle: 'alpha' }, - { id: 'org-2', name: 'Beta', orgHandle: 'beta' }, + const options: {filter: string; limit: number} = {filter: 'type eq "TENANT"', limit: 25}; + const orgs: Organization[] = [ + {id: 'org-1', name: 'Alpha', orgHandle: 'alpha'}, + {id: 'org-2', name: 'Beta', orgHandle: 'beta'}, ]; beforeEach(() => { @@ -63,7 +63,7 @@ describe('getMyOrganizations (Next.js server action)', () => { }); it('should return organizations when sessionId is provided (no getSessionId fallback)', async () => { - const result = await getMyOrganizations(options, 'sess-123'); + const result: Organization[] = await getMyOrganizations(options, 'sess-123'); expect(AsgardeoNextClient.getInstance).toHaveBeenCalledTimes(1); expect(getSessionId).not.toHaveBeenCalled(); @@ -73,7 +73,7 @@ describe('getMyOrganizations (Next.js server action)', () => { }); it('should fall back to getSessionId when sessionId is undefined', async () => { - const result = await getMyOrganizations(options, undefined); + const result: Organization[] = await getMyOrganizations(options, undefined); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.getAccessToken).toHaveBeenCalledWith('sess-abc'); @@ -82,7 +82,7 @@ describe('getMyOrganizations (Next.js server action)', () => { }); it('should fall back to getSessionId when sessionId is null', async () => { - const result = await getMyOrganizations(options, null as unknown as string); + const result: Organization[] = await getMyOrganizations(options, null as unknown as string); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.getAccessToken).toHaveBeenCalledWith('sess-abc'); @@ -91,7 +91,7 @@ describe('getMyOrganizations (Next.js server action)', () => { }); it('should treat empty string sessionId as falsy and calls getSessionId', async () => { - const result = await getMyOrganizations(options, ''); + const result: Organization[] = await getMyOrganizations(options, ''); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.getAccessToken).toHaveBeenCalledWith('sess-abc'); @@ -100,7 +100,7 @@ describe('getMyOrganizations (Next.js server action)', () => { }); it('should pass through undefined options', async () => { - const result = await getMyOrganizations(undefined, 'sess-123'); + const result: Organization[] = await getMyOrganizations(undefined, 'sess-123'); expect(mockClient.getMyOrganizations).toHaveBeenCalledWith(undefined, 'sess-123'); expect(result).toEqual(orgs); @@ -111,8 +111,8 @@ describe('getMyOrganizations (Next.js server action)', () => { await expect(getMyOrganizations(options, undefined)).rejects.toMatchObject({ constructor: AsgardeoAPIError, - statusCode: 401, message: expect.stringContaining('Failed to get the organizations for the user: No session ID available'), + statusCode: 401, }); // Should fail before calling client methods @@ -125,11 +125,14 @@ describe('getMyOrganizations (Next.js server action)', () => { await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ constructor: AsgardeoAPIError, + message: expect.stringContaining( + 'Failed to get the organizations for the user: User is not signed in - access token retrieval failed', + ), statusCode: 401, - message: expect.stringContaining('Failed to get the organizations for the user: User is not signed in - access token retrieval failed'), }); expect(mockClient.getAccessToken).toHaveBeenCalledWith('sess-123'); + // eslint-disable-next-line no-console expect(console.error).toHaveBeenCalled(); // inner catch logs expect(mockClient.getMyOrganizations).not.toHaveBeenCalled(); }); @@ -139,10 +142,13 @@ describe('getMyOrganizations (Next.js server action)', () => { await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ constructor: AsgardeoAPIError, + message: expect.stringContaining( + 'Failed to get the organizations for the user: User is not signed in - access token retrieval failed', + ), statusCode: 401, - message: expect.stringContaining('Failed to get the organizations for the user: User is not signed in - access token retrieval failed'), }); + // eslint-disable-next-line no-console expect(console.error).toHaveBeenCalled(); expect(mockClient.getMyOrganizations).not.toHaveBeenCalled(); }); @@ -152,22 +158,25 @@ describe('getMyOrganizations (Next.js server action)', () => { await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ constructor: AsgardeoAPIError, + message: expect.stringContaining( + 'Failed to get the organizations for the user: User is not signed in - access token retrieval failed', + ), statusCode: 401, - message: expect.stringContaining('Failed to get the organizations for the user: User is not signed in - access token retrieval failed'), }); + // eslint-disable-next-line no-console expect(console.error).toHaveBeenCalled(); expect(mockClient.getMyOrganizations).not.toHaveBeenCalled(); }); it('should wrap an AsgardeoAPIError from client.getMyOrganizations, preserving statusCode', async () => { - const upstream = new AsgardeoAPIError('Upstream failed', 'ORG_LIST_503', 'server', 503); + const upstream: AsgardeoAPIError = new AsgardeoAPIError('Upstream failed', 'ORG_LIST_503', 'server', 503); mockClient.getMyOrganizations.mockRejectedValueOnce(upstream); await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ constructor: AsgardeoAPIError, - statusCode: 503, message: expect.stringContaining('Failed to get the organizations for the user: Upstream failed'), + statusCode: 503, }); }); @@ -176,8 +185,8 @@ describe('getMyOrganizations (Next.js server action)', () => { await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ constructor: AsgardeoAPIError, - statusCode: undefined, message: expect.stringContaining('Failed to get the organizations for the user: network down'), + statusCode: undefined, }); }); @@ -188,13 +197,13 @@ describe('getMyOrganizations (Next.js server action)', () => { await expect(getMyOrganizations(options, 'sess-123')).rejects.toMatchObject({ constructor: AsgardeoAPIError, - statusCode: undefined, message: expect.stringContaining('Failed to get the organizations for the user: factory failed'), + statusCode: undefined, }); }); it('should handle minimal call: no options, undefined sessionId -> resolves via getSessionId and succeeds', async () => { - const result = await getMyOrganizations(); + const result: Organization[] = await getMyOrganizations(); expect(getSessionId).toHaveBeenCalledTimes(1); expect(mockClient.getAccessToken).toHaveBeenCalledWith('sess-abc'); @@ -202,4 +211,3 @@ describe('getMyOrganizations (Next.js server action)', () => { expect(result).toEqual(orgs); }); }); - diff --git a/packages/nextjs/src/server/actions/__tests__/getOrganizationAction.test.ts b/packages/nextjs/src/server/actions/__tests__/getOrganizationAction.test.ts index 7d117b2ac..e55d72f8e 100644 --- a/packages/nextjs/src/server/actions/__tests__/getOrganizationAction.test.ts +++ b/packages/nextjs/src/server/actions/__tests__/getOrganizationAction.test.ts @@ -1,5 +1,26 @@ -// src/server/actions/__tests__/getOrganizationAction.test.ts -import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest'; +/** + * 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, afterEach, type Mock} from 'vitest'; + +// Import SUT and mocked deps +import AsgardeoNextClient from '../../../AsgardeoNextClient'; +import getOrganizationAction from '../getOrganizationAction'; // Mock client factory BEFORE importing the SUT vi.mock('../../../AsgardeoNextClient', () => ({ @@ -8,21 +29,19 @@ vi.mock('../../../AsgardeoNextClient', () => ({ }, })); -// Import SUT and mocked deps -import getOrganizationAction from '../getOrganizationAction'; -import AsgardeoNextClient from '../../../AsgardeoNextClient'; - // Minimal shape for testing; add fields only if you assert on them -type OrganizationDetails = { id: string; name: string; orgHandle?: string }; +type OrganizationDetails = {id: string; name: string; orgHandle?: string}; + +type ActionResult = Awaited>; describe('getOrganizationAction', () => { - const mockClient = { + const mockClient: {getOrganization: ReturnType} = { getOrganization: vi.fn(), }; - const orgId = 'org-001'; - const sessionId = 'sess-123'; - const org: OrganizationDetails = { id: orgId, name: 'Alpha', orgHandle: 'alpha' }; + const orgId: string = 'org-001'; + const sessionId: string = 'sess-123'; + const org: OrganizationDetails = {id: orgId, name: 'Alpha', orgHandle: 'alpha'}; beforeEach(() => { vi.resetAllMocks(); @@ -36,22 +55,22 @@ describe('getOrganizationAction', () => { it('should return success with organization when upstream succeeds', async () => { mockClient.getOrganization.mockResolvedValueOnce(org); - const result = await getOrganizationAction(orgId, sessionId); + const result: ActionResult = await getOrganizationAction(orgId, sessionId); expect(AsgardeoNextClient.getInstance).toHaveBeenCalledTimes(1); expect(mockClient.getOrganization).toHaveBeenCalledWith(orgId, sessionId); expect(result).toEqual({ - success: true, - data: { organization: org }, + data: {organization: org}, error: null, + success: true, }); }); it('should pass through empty-string organizationId and sessionId (documents current behavior)', async () => { mockClient.getOrganization.mockResolvedValueOnce(org); - const result = await getOrganizationAction('', ''); + const result: ActionResult = await getOrganizationAction('', ''); expect(mockClient.getOrganization).toHaveBeenCalledWith('', ''); expect(result.success).toBe(true); @@ -61,12 +80,12 @@ describe('getOrganizationAction', () => { it('should return failure shape when client.getOrganization rejects', async () => { mockClient.getOrganization.mockRejectedValueOnce(new Error('upstream down')); - const result = await getOrganizationAction(orgId, sessionId); + const result: ActionResult = await getOrganizationAction(orgId, sessionId); expect(result).toEqual({ - success: false, - data: { user: {} }, + data: {user: {}}, error: 'Failed to get organization', + success: false, }); }); @@ -75,31 +94,30 @@ describe('getOrganizationAction', () => { throw new Error('factory failed'); }); - const result = await getOrganizationAction(orgId, sessionId); + const result: ActionResult = await getOrganizationAction(orgId, sessionId); expect(result).toEqual({ - success: false, - data: { user: {} }, + data: {user: {}}, error: 'Failed to get organization', + success: false, }); }); it('should return failure shape when client rejects with a non-Error value', async () => { - mockClient.getOrganization.mockRejectedValueOnce('bad'); - const result = await getOrganizationAction(orgId, sessionId); - expect(result).toEqual({ - success: false, - data: { user: {} }, - error: 'Failed to get organization', + mockClient.getOrganization.mockRejectedValueOnce('bad'); + const result: ActionResult = await getOrganizationAction(orgId, sessionId); + expect(result).toEqual({ + data: {user: {}}, + error: 'Failed to get organization', + success: false, + }); }); -}); - it('should not mutate the organization object returned by upstream', async () => { - const upstreamOrg = { ...org, extra: { nested: true } }; + const upstreamOrg: OrganizationDetails & {extra: {nested: boolean}} = {...org, extra: {nested: true}}; mockClient.getOrganization.mockResolvedValueOnce(upstreamOrg); - const result = await getOrganizationAction(orgId, sessionId); + const result: ActionResult = await getOrganizationAction(orgId, sessionId); expect(result.data.organization).toEqual(upstreamOrg); }); diff --git a/packages/nextjs/src/server/actions/getAccessToken.ts b/packages/nextjs/src/server/actions/getAccessToken.ts index 0fe5da931..9d7a351fe 100644 --- a/packages/nextjs/src/server/actions/getAccessToken.ts +++ b/packages/nextjs/src/server/actions/getAccessToken.ts @@ -20,7 +20,7 @@ import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies'; import {cookies} from 'next/headers'; -import SessionManager from '../../utils/SessionManager'; +import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager'; /** * Get the access token from the session cookie. @@ -30,11 +30,11 @@ import SessionManager from '../../utils/SessionManager'; const getAccessToken = async (): Promise => { const cookieStore: ReadonlyRequestCookies = await cookies(); - const sessionToken = cookieStore.get(SessionManager.getSessionCookieName())?.value; + const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName())?.value; if (sessionToken) { try { - const sessionPayload = await SessionManager.verifySessionToken(sessionToken); + const sessionPayload: SessionTokenPayload = await SessionManager.verifySessionToken(sessionToken); return sessionPayload['accessToken'] as string; } catch (error) { diff --git a/packages/nextjs/src/server/actions/getBrandingPreference.ts b/packages/nextjs/src/server/actions/getBrandingPreference.ts index b2949f199..5b2ac8807 100644 --- a/packages/nextjs/src/server/actions/getBrandingPreference.ts +++ b/packages/nextjs/src/server/actions/getBrandingPreference.ts @@ -30,7 +30,7 @@ import { */ const getBrandingPreference = async ( config: GetBrandingPreferenceConfig, - sessionId?: string | undefined, + sessionId?: string | undefined, // eslint-disable-line @typescript-eslint/no-unused-vars ): Promise => { try { return await baseGetBrandingPreference(config); diff --git a/packages/nextjs/src/server/actions/getClientOrigin.ts b/packages/nextjs/src/server/actions/getClientOrigin.ts index 2d8d65471..dfe90abc7 100644 --- a/packages/nextjs/src/server/actions/getClientOrigin.ts +++ b/packages/nextjs/src/server/actions/getClientOrigin.ts @@ -1,11 +1,30 @@ +/** + * 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. + */ + 'use server'; +import {ReadonlyHeaders} from 'next/dist/server/web/spec-extension/adapters/headers'; import {headers} from 'next/headers'; -const getClientOrigin = async () => { - const headersList = await headers(); - const host = headersList.get('host'); - const protocol = headersList.get('x-forwarded-proto') ?? 'http'; +const getClientOrigin = async (): Promise => { + const headersList: ReadonlyHeaders = await headers(); + const host: string | null = headersList.get('host'); + const protocol: string = headersList.get('x-forwarded-proto') ?? 'http'; return `${protocol}://${host}`; }; diff --git a/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts b/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts index bd08d9c7a..a355ab90c 100644 --- a/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts +++ b/packages/nextjs/src/server/actions/getCurrentOrganizationAction.ts @@ -18,24 +18,30 @@ 'use server'; -import {Organization, OrganizationDetails} from '@asgardeo/node'; +import {Organization} from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; /** * Server action to create an organization. */ -const getCurrentOrganizationAction = async (sessionId: string) => { +const getCurrentOrganizationAction = async ( + sessionId: string, +): Promise<{ + data: {organization?: Organization; user?: Record}; + error: string | null; + success: boolean; +}> => { try { - const client = AsgardeoNextClient.getInstance(); - const organization: Organization = await client.getCurrentOrganization(sessionId) as Organization; - return {success: true, data: {organization}, error: null}; + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const organization: Organization = (await client.getCurrentOrganization(sessionId)) as Organization; + return {data: {organization}, error: null, success: true}; } catch (error) { return { - success: false, data: { user: {}, }, error: 'Failed to get the current organization', + success: false, }; } }; diff --git a/packages/nextjs/src/server/actions/getMyOrganizations.ts b/packages/nextjs/src/server/actions/getMyOrganizations.ts index f7d9b123c..eb9f62aee 100644 --- a/packages/nextjs/src/server/actions/getMyOrganizations.ts +++ b/packages/nextjs/src/server/actions/getMyOrganizations.ts @@ -26,10 +26,10 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; */ const getMyOrganizations = async (options?: any, sessionId?: string | undefined): Promise => { try { - const client = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); // Get session ID if not provided - let resolvedSessionId = sessionId; + let resolvedSessionId: string | undefined = sessionId; if (!resolvedSessionId) { // Import getSessionId locally to avoid circular dependencies const {default: getSessionId} = await import('./getSessionId'); @@ -47,7 +47,7 @@ const getMyOrganizations = async (options?: any, sessionId?: string | undefined) // Check if user is signed in by trying to get access token try { - const accessToken = await client.getAccessToken(resolvedSessionId); + const accessToken: string = await client.getAccessToken(resolvedSessionId); if (!accessToken) { throw new AsgardeoAPIError( @@ -58,6 +58,7 @@ const getMyOrganizations = async (options?: any, sessionId?: string | undefined) ); } } catch (error) { + // eslint-disable-next-line no-console console.error('[getMyOrganizations] Failed to get access token:', error); throw new AsgardeoAPIError( 'User is not signed in - access token retrieval failed', diff --git a/packages/nextjs/src/server/actions/getOrganizationAction.ts b/packages/nextjs/src/server/actions/getOrganizationAction.ts index e5eb99d60..d83491e17 100644 --- a/packages/nextjs/src/server/actions/getOrganizationAction.ts +++ b/packages/nextjs/src/server/actions/getOrganizationAction.ts @@ -24,18 +24,25 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; /** * Server action to create an organization. */ -const getOrganizationAction = async (organizationId: string, sessionId: string) => { +const getOrganizationAction = async ( + organizationId: string, + sessionId: string, +): Promise<{ + data: {organization?: OrganizationDetails; user?: Record}; + error: string | null; + success: boolean; +}> => { try { - const client = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); const organization: OrganizationDetails = await client.getOrganization(organizationId, sessionId); - return {success: true, data: {organization}, error: null}; + return {data: {organization}, error: null, success: true}; } catch (error) { return { - success: false, data: { user: {}, }, error: 'Failed to get organization', + success: false, }; } }; diff --git a/packages/nextjs/src/server/actions/getSessionId.ts b/packages/nextjs/src/server/actions/getSessionId.ts index 388e3c0ad..e3b598c30 100644 --- a/packages/nextjs/src/server/actions/getSessionId.ts +++ b/packages/nextjs/src/server/actions/getSessionId.ts @@ -20,7 +20,7 @@ import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies'; import {cookies} from 'next/headers'; -import SessionManager from '../../utils/SessionManager'; +import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager'; /** * Get the session ID from cookies. @@ -31,11 +31,11 @@ import SessionManager from '../../utils/SessionManager'; const getSessionId = async (): Promise => { const cookieStore: ReadonlyRequestCookies = await cookies(); - const sessionToken = cookieStore.get(SessionManager.getSessionCookieName())?.value; + const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName())?.value; if (sessionToken) { try { - const sessionPayload = await SessionManager.verifySessionToken(sessionToken); + const sessionPayload: SessionTokenPayload = await SessionManager.verifySessionToken(sessionToken); return sessionPayload.sessionId; } catch (error) { diff --git a/packages/nextjs/src/server/actions/getSessionPayload.ts b/packages/nextjs/src/server/actions/getSessionPayload.ts index 108475bc6..7cdea1d73 100644 --- a/packages/nextjs/src/server/actions/getSessionPayload.ts +++ b/packages/nextjs/src/server/actions/getSessionPayload.ts @@ -31,7 +31,7 @@ import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager'; const getSessionPayload = async (): Promise => { const cookieStore: ReadonlyRequestCookies = await cookies(); - const sessionToken = cookieStore.get(SessionManager.getSessionCookieName())?.value; + const sessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName())?.value; if (!sessionToken) { return undefined; } diff --git a/packages/nextjs/src/server/actions/getUserAction.ts b/packages/nextjs/src/server/actions/getUserAction.ts index 3abcb9b4b..a0ad5e4ba 100644 --- a/packages/nextjs/src/server/actions/getUserAction.ts +++ b/packages/nextjs/src/server/actions/getUserAction.ts @@ -18,19 +18,22 @@ 'use server'; +import {User} from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; /** * Server action to get the current user. * Returns the user profile if signed in. */ -const getUserAction = async (sessionId: string) => { +const getUserAction = async ( + sessionId: string, +): Promise<{data: {user: User | null}; error: string | null; success: boolean}> => { try { - const client = AsgardeoNextClient.getInstance(); - const user = await client.getUser(sessionId); - return {success: true, data: {user}, error: null}; + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const user: User = await client.getUser(sessionId); + return {data: {user}, error: null, success: true}; } catch (error) { - return {success: false, data: {user: null}, error: 'Failed to get user'}; + return {data: {user: null}, error: 'Failed to get user', success: false}; } }; diff --git a/packages/nextjs/src/server/actions/getUserProfileAction.ts b/packages/nextjs/src/server/actions/getUserProfileAction.ts index 3b64bf502..983bc0375 100644 --- a/packages/nextjs/src/server/actions/getUserProfileAction.ts +++ b/packages/nextjs/src/server/actions/getUserProfileAction.ts @@ -25,22 +25,24 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; * Server action to get the current user. * Returns the user profile if signed in. */ -const getUserProfileAction = async (sessionId: string) => { +const getUserProfileAction = async ( + sessionId: string, +): Promise<{data: {userProfile: UserProfile}; error: string | null; success: boolean}> => { try { - const client = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); const updatedProfile: UserProfile = await client.getUserProfile(sessionId); - return {success: true, data: {userProfile: updatedProfile}, error: null}; + return {data: {userProfile: updatedProfile}, error: null, success: true}; } catch (error) { return { - success: false, data: { userProfile: { - schemas: [], - profile: {}, flattenedProfile: {}, + profile: {}, + schemas: [], }, }, error: 'Failed to get user profile', + success: false, }; } }; diff --git a/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts b/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts index bef279708..5acfdbbb1 100644 --- a/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts +++ b/packages/nextjs/src/server/actions/handleOAuthCallbackAction.ts @@ -18,10 +18,13 @@ 'use server'; +import {IdToken} from '@asgardeo/node'; +import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies'; import {cookies} from 'next/headers'; import AsgardeoNextClient from '../../AsgardeoNextClient'; -import SessionManager from '../../utils/SessionManager'; +import {AsgardeoNextConfig} from '../../models/config'; import logger from '../../utils/logger'; +import SessionManager from '../../utils/SessionManager'; /** * Server action to handle OAuth callback with authorization code. @@ -38,35 +41,35 @@ const handleOAuthCallbackAction = async ( state: string, sessionState?: string, ): Promise<{ - success: boolean; error?: string; redirectUrl?: string; + success: boolean; }> => { try { if (!code || !state) { return { - success: false, error: 'Missing required OAuth parameters: code and state are required', + success: false, }; } - const asgardeoClient = AsgardeoNextClient.getInstance(); + const asgardeoClient: AsgardeoNextClient = AsgardeoNextClient.getInstance(); if (!asgardeoClient.isInitialized) { return { - success: false, error: 'Asgardeo client is not initialized', + success: false, }; } - const cookieStore = await cookies(); + const cookieStore: ReadonlyRequestCookies = await cookies(); let sessionId: string | undefined; - const tempSessionToken = cookieStore.get(SessionManager.getTempSessionCookieName())?.value; + const tempSessionToken: string | undefined = cookieStore.get(SessionManager.getTempSessionCookieName())?.value; if (tempSessionToken) { try { - const tempSession = await SessionManager.verifyTempSession(tempSessionToken); + const tempSession: {sessionId: string} = await SessionManager.verifyTempSession(tempSessionToken); sessionId = tempSession.sessionId; } catch { logger.error( @@ -79,13 +82,13 @@ const handleOAuthCallbackAction = async ( logger.error('[handleOAuthCallbackAction] No session ID found in cookies or temporary session token.'); return { - success: false, error: 'No session found. Please start the authentication flow again.', + success: false, }; } // Exchange the authorization code for tokens - const signInResult = await asgardeoClient.signIn( + const signInResult: Record = await asgardeoClient.signIn( { code, session_state: sessionState, @@ -97,16 +100,18 @@ const handleOAuthCallbackAction = async ( if (signInResult) { try { - const idToken = await asgardeoClient.getDecodedIdToken( + const idToken: IdToken = await asgardeoClient.getDecodedIdToken( sessionId, - signInResult['id_token'] || signInResult['idToken'], + (signInResult['id_token'] || signInResult['idToken']) as string, ); - const accessToken: string = signInResult['accessToken'] || signInResult['access_token']; - const userIdFromToken = idToken.sub || signInResult['sub'] || sessionId; - const scopes = signInResult['scope']; - const organizationId = idToken['user_org'] || idToken['organization_id']; - - const sessionToken = await SessionManager.createSessionToken( + const accessToken: string = (signInResult['accessToken'] || signInResult['access_token']) as string; + const userIdFromToken: string = (idToken.sub || signInResult['sub'] || sessionId) as string; + const scopes: string = signInResult['scope'] as string; + const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as + | string + | undefined; + + const sessionToken: string = await SessionManager.createSessionToken( accessToken, userIdFromToken, sessionId, @@ -125,17 +130,17 @@ const handleOAuthCallbackAction = async ( } } - const config = await asgardeoClient.getConfiguration(); - const afterSignInUrl = config.afterSignInUrl || '/'; + const config: AsgardeoNextConfig = await asgardeoClient.getConfiguration(); + const afterSignInUrl: string = config.afterSignInUrl || '/'; return { - success: true, redirectUrl: afterSignInUrl, + success: true, }; } catch (error) { return { - success: false, error: error instanceof Error ? error.message : 'Authentication failed', + success: false, }; } }; diff --git a/packages/nextjs/src/server/actions/isSignedIn.ts b/packages/nextjs/src/server/actions/isSignedIn.ts index adcd70114..6c0d43780 100644 --- a/packages/nextjs/src/server/actions/isSignedIn.ts +++ b/packages/nextjs/src/server/actions/isSignedIn.ts @@ -18,9 +18,10 @@ 'use server'; -import AsgardeoNextClient from '../../AsgardeoNextClient'; import getSessionId from './getSessionId'; import getSessionPayload from './getSessionPayload'; +import AsgardeoNextClient from '../../AsgardeoNextClient'; +import {SessionTokenPayload} from '../../utils/SessionManager'; /** * Check if the user is currently signed in. @@ -31,15 +32,15 @@ import getSessionPayload from './getSessionPayload'; */ const isSignedIn = async (sessionId?: string): Promise => { try { - const sessionPayload = await getSessionPayload(); + const sessionPayload: SessionTokenPayload | undefined = await getSessionPayload(); if (sessionPayload) { - const resolvedSessionId = sessionPayload.sessionId; + const resolvedSessionId: string = sessionPayload.sessionId; if (resolvedSessionId) { - const client = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); try { - const accessToken = await client.getAccessToken(resolvedSessionId); + const accessToken: string = await client.getAccessToken(resolvedSessionId); return !!accessToken; } catch (error) { return false; @@ -47,16 +48,16 @@ const isSignedIn = async (sessionId?: string): Promise => { } } - const resolvedSessionId = sessionId || (await getSessionId()); + const resolvedSessionId: string | undefined = sessionId || (await getSessionId()); if (!resolvedSessionId) { return false; } - const client = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); try { - const accessToken = await client.getAccessToken(resolvedSessionId); + const accessToken: string = await client.getAccessToken(resolvedSessionId); return !!accessToken; } catch (error) { diff --git a/packages/nextjs/src/server/actions/signInAction.ts b/packages/nextjs/src/server/actions/signInAction.ts index 69ba63533..98ca3412e 100644 --- a/packages/nextjs/src/server/actions/signInAction.ts +++ b/packages/nextjs/src/server/actions/signInAction.ts @@ -18,17 +18,19 @@ 'use server'; -import {cookies} from 'next/headers'; import { generateSessionId, EmbeddedSignInFlowStatus, EmbeddedSignInFlowHandleRequestPayload, EmbeddedFlowExecuteRequestConfig, EmbeddedSignInFlowInitiateResponse, + IdToken, isEmpty, } from '@asgardeo/node'; +import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies'; +import {cookies} from 'next/headers'; import AsgardeoNextClient from '../../AsgardeoNextClient'; -import SessionManager from '../../utils/SessionManager'; +import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager'; /** * Server action for signing in a user. @@ -42,7 +44,6 @@ const signInAction = async ( payload?: EmbeddedSignInFlowHandleRequestPayload, request?: EmbeddedFlowExecuteRequestConfig, ): Promise<{ - success: boolean; data?: | { afterSignInUrl?: string; @@ -50,32 +51,31 @@ const signInAction = async ( } | EmbeddedSignInFlowInitiateResponse; error?: string; + success: boolean; }> => { try { - const client = AsgardeoNextClient.getInstance(); - const cookieStore = await cookies(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const cookieStore: ReadonlyRequestCookies = await cookies(); let sessionId: string | undefined; - let userId: string | undefined; - const existingSessionToken = cookieStore.get(SessionManager.getSessionCookieName())?.value; + const existingSessionToken: string | undefined = cookieStore.get(SessionManager.getSessionCookieName())?.value; if (existingSessionToken) { try { - const sessionPayload = await SessionManager.verifySessionToken(existingSessionToken); + const sessionPayload: SessionTokenPayload = await SessionManager.verifySessionToken(existingSessionToken); sessionId = sessionPayload.sessionId; - userId = sessionPayload.sub; } catch { // Invalid session token, will create new temp session } } if (!sessionId) { - const tempSessionToken = cookieStore.get(SessionManager.getTempSessionCookieName())?.value; + const tempSessionToken: string | undefined = cookieStore.get(SessionManager.getTempSessionCookieName())?.value; if (tempSessionToken) { try { - const tempSession = await SessionManager.verifyTempSession(tempSessionToken); + const tempSession: {sessionId: string} = await SessionManager.verifyTempSession(tempSessionToken); sessionId = tempSession.sessionId; } catch { // Invalid temp session, will create new one @@ -86,7 +86,7 @@ const signInAction = async ( if (!sessionId) { sessionId = generateSessionId(); - const tempSessionToken = await SessionManager.createTempSession(sessionId); + const tempSessionToken: string = await SessionManager.createTempSession(sessionId); cookieStore.set( SessionManager.getTempSessionCookieName(), @@ -97,15 +97,15 @@ const signInAction = async ( // If no payload provided, redirect to sign-in URL for redirect-based sign-in. if (!payload || isEmpty(payload)) { - const defaultSignInUrl = await client.getAuthorizeRequestUrl({}, sessionId); - return {success: true, data: {signInUrl: String(defaultSignInUrl)}}; + const defaultSignInUrl: string = await client.getAuthorizeRequestUrl({}, sessionId); + return {data: {signInUrl: String(defaultSignInUrl)}, success: true}; } // Handle embedded sign-in flow const response: any = await client.signIn(payload, request!, sessionId); if (response.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) { - const signInResult = await client.signIn( + const signInResult: Record = await client.signIn( { code: response?.authData?.code, session_state: response?.authData?.session_state, @@ -116,13 +116,15 @@ const signInAction = async ( ); if (signInResult) { - const idToken = await client.getDecodedIdToken(sessionId); - const userIdFromToken = idToken['sub'] || signInResult['sub'] || sessionId; - const accessToken = signInResult['accessToken']; - const scopes = signInResult['scope']; - const organizationId = idToken['user_org'] || idToken['organization_id']; - - const sessionToken = await SessionManager.createSessionToken( + const idToken: IdToken = await client.getDecodedIdToken(sessionId); + const userIdFromToken: string = (idToken['sub'] || signInResult['sub'] || sessionId) as string; + const {accessToken}: {accessToken: string} = signInResult as {accessToken: string}; + const scopes: string = signInResult['scope'] as string; + const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as + | string + | undefined; + + const sessionToken: string = await SessionManager.createSessionToken( accessToken, userIdFromToken, sessionId as string, @@ -135,14 +137,15 @@ const signInAction = async ( cookieStore.delete(SessionManager.getTempSessionCookieName()); } - const afterSignInUrl = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); - return {success: true, data: {afterSignInUrl: String(afterSignInUrl)}}; + const afterSignInUrl: string = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); + return {data: {afterSignInUrl: String(afterSignInUrl)}, success: true}; } - return {success: true, data: response as EmbeddedSignInFlowInitiateResponse}; + return {data: response as EmbeddedSignInFlowInitiateResponse, success: true}; } catch (error) { + // eslint-disable-next-line no-console console.error('[signInAction] Error during sign-in:', error); - return {success: false, error: String(error)}; + return {error: String(error), success: false}; } }; diff --git a/packages/nextjs/src/server/actions/signOutAction.ts b/packages/nextjs/src/server/actions/signOutAction.ts index 2103b6a29..6d76e13ea 100644 --- a/packages/nextjs/src/server/actions/signOutAction.ts +++ b/packages/nextjs/src/server/actions/signOutAction.ts @@ -18,11 +18,12 @@ 'use server'; +import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies'; import {cookies} from 'next/headers'; -import AsgardeoNextClient from '../../AsgardeoNextClient'; -import SessionManager from '../../utils/SessionManager'; import getSessionId from './getSessionId'; +import AsgardeoNextClient from '../../AsgardeoNextClient'; import logger from '../../utils/logger'; +import SessionManager from '../../utils/SessionManager'; /** * Server action for signing out a user. @@ -30,19 +31,19 @@ import logger from '../../utils/logger'; * * @returns Promise that resolves with success status and optional after sign-out URL */ -const signOutAction = async (): Promise<{success: boolean; data?: {afterSignOutUrl?: string}; error?: unknown}> => { +const signOutAction = async (): Promise<{data?: {afterSignOutUrl?: string}; error?: unknown; success: boolean}> => { logger.debug('[signOutAction] Initiating sign out process from the server action.'); - const clearSessionCookies = async () => { - const cookieStore = await cookies(); + const clearSessionCookies = async (): Promise => { + const cookieStore: ReadonlyRequestCookies = await cookies(); cookieStore.delete(SessionManager.getSessionCookieName()); cookieStore.delete(SessionManager.getTempSessionCookieName()); }; try { - const client = AsgardeoNextClient.getInstance(); - const sessionId = await getSessionId(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); + const sessionId: string | undefined = await getSessionId(); let afterSignOutUrl: string = '/'; @@ -54,7 +55,7 @@ const signOutAction = async (): Promise<{success: boolean; data?: {afterSignOutU await clearSessionCookies(); - return {success: true, data: {afterSignOutUrl}}; + return {data: {afterSignOutUrl}, success: true}; } catch (error) { logger.error('[signOutAction] Error during sign out from the server action:', error); @@ -62,9 +63,18 @@ const signOutAction = async (): Promise<{success: boolean; data?: {afterSignOutU await clearSessionCookies(); + let errorMessage: unknown; + if (typeof error === 'string') { + errorMessage = error; + } else if (error instanceof Error) { + errorMessage = error.message; + } else { + errorMessage = JSON.stringify(error); + } + return { + error: errorMessage, success: false, - error: typeof error === 'string' ? error : error instanceof Error ? error.message : JSON.stringify(error), }; } }; diff --git a/packages/nextjs/src/server/actions/signUpAction.ts b/packages/nextjs/src/server/actions/signUpAction.ts index 2e9dd7f69..febd32c03 100644 --- a/packages/nextjs/src/server/actions/signUpAction.ts +++ b/packages/nextjs/src/server/actions/signUpAction.ts @@ -18,12 +18,7 @@ 'use server'; -import { - EmbeddedFlowExecuteRequestConfig, - EmbeddedFlowExecuteRequestPayload, - EmbeddedFlowExecuteResponse, - EmbeddedFlowStatus, -} from '@asgardeo/node'; +import {EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteResponse, EmbeddedFlowStatus} from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; /** @@ -36,9 +31,7 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; */ const signUpAction = async ( payload?: EmbeddedFlowExecuteRequestPayload, - request?: EmbeddedFlowExecuteRequestConfig, ): Promise<{ - success: boolean; data?: | { afterSignUpUrl?: string; @@ -46,29 +39,29 @@ const signUpAction = async ( } | EmbeddedFlowExecuteResponse; error?: string; + success: boolean; }> => { try { - const client = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); // If no payload provided, redirect to sign-in URL for redirect-based sign-in. // If there's a payload, handle the embedded sign-in flow. if (!payload) { - const defaultSignUpUrl = ''; - - return {success: true, data: {signUpUrl: String(defaultSignUpUrl)}}; - } else { - const response: any = await client.signUp(payload); + const defaultSignUpUrl: string = ''; - if (response.flowStatus === EmbeddedFlowStatus.Complete) { - const afterSignUpUrl = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); + return {data: {signUpUrl: String(defaultSignUpUrl)}, success: true}; + } + const response: any = await client.signUp(payload); - return {success: true, data: {afterSignUpUrl: String(afterSignUpUrl)}}; - } + if (response.flowStatus === EmbeddedFlowStatus.Complete) { + const afterSignUpUrl: string = await (await client.getStorageManager()).getConfigDataParameter('afterSignInUrl'); - return {success: true, data: response as EmbeddedFlowExecuteResponse}; + return {data: {afterSignUpUrl: String(afterSignUpUrl)}, success: true}; } + + return {data: response as EmbeddedFlowExecuteResponse, success: true}; } catch (error) { - return {success: false, error: String(error)}; + return {error: String(error), success: false}; } }; diff --git a/packages/nextjs/src/server/actions/switchOrganization.ts b/packages/nextjs/src/server/actions/switchOrganization.ts index 3cd9c8562..64d095051 100644 --- a/packages/nextjs/src/server/actions/switchOrganization.ts +++ b/packages/nextjs/src/server/actions/switchOrganization.ts @@ -18,12 +18,13 @@ 'use server'; -import {Organization, AsgardeoAPIError, TokenResponse} from '@asgardeo/node'; +import {Organization, AsgardeoAPIError, IdToken, TokenResponse} from '@asgardeo/node'; +import {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies'; +import {cookies} from 'next/headers'; import getSessionId from './getSessionId'; import AsgardeoNextClient from '../../AsgardeoNextClient'; import logger from '../../utils/logger'; import SessionManager from '../../utils/SessionManager'; -import {cookies} from 'next/headers'; /** * Server action to switch organization. @@ -33,10 +34,10 @@ const switchOrganization = async ( sessionId: string | undefined, ): Promise => { try { - const cookieStore = await cookies(); + const cookieStore: ReadonlyRequestCookies = await cookies(); const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); - const _sessionId: string = sessionId ?? ((await getSessionId()) as string); - const response: TokenResponse | Response = await client.switchOrganization(organization, _sessionId); + const resolvedSessionId: string = sessionId ?? ((await getSessionId()) as string); + const response: TokenResponse | Response = await client.switchOrganization(organization, resolvedSessionId); // After switching organization, we need to refresh the page to get updated session data // This is because server components don't maintain state between function calls @@ -46,16 +47,18 @@ const switchOrganization = async ( revalidatePath('/'); if (response) { - const idToken = await client.getDecodedIdToken(_sessionId, (response as TokenResponse).idToken); - const userIdFromToken = idToken['sub']; - const accessToken = (response as TokenResponse).accessToken; - const scopes = (response as TokenResponse).scope; - const organizationId = idToken['user_org'] || idToken['organization_id']; + const idToken: IdToken = await client.getDecodedIdToken(resolvedSessionId, (response as TokenResponse).idToken); + const userIdFromToken: string = idToken['sub'] as string; + const {accessToken}: {accessToken: string} = response as TokenResponse; + const scopes: string = (response as TokenResponse).scope; + const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as + | string + | undefined; - const sessionToken = await SessionManager.createSessionToken( + const sessionToken: string = await SessionManager.createSessionToken( accessToken, userIdFromToken as string, - _sessionId as string, + resolvedSessionId as string, scopes, organizationId, ); diff --git a/packages/nextjs/src/server/actions/updateUserProfileAction.ts b/packages/nextjs/src/server/actions/updateUserProfileAction.ts index 69ec47d5b..33937b376 100644 --- a/packages/nextjs/src/server/actions/updateUserProfileAction.ts +++ b/packages/nextjs/src/server/actions/updateUserProfileAction.ts @@ -18,7 +18,7 @@ 'use server'; -import {UpdateMeProfileConfig, User, UserProfile} from '@asgardeo/node'; +import {UpdateMeProfileConfig, User} from '@asgardeo/node'; import AsgardeoNextClient from '../../AsgardeoNextClient'; /** @@ -28,18 +28,18 @@ import AsgardeoNextClient from '../../AsgardeoNextClient'; const updateUserProfileAction = async ( payload: UpdateMeProfileConfig, sessionId?: string, -): Promise<{success: boolean; data: {user: User}; error: string}> => { +): Promise<{data: {user: User}; error: string; success: boolean}> => { try { - const client = AsgardeoNextClient.getInstance(); + const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); const user: User = await client.updateUserProfile(payload, sessionId); - return {success: true, data: {user}, error: ""}; + return {data: {user}, error: '', success: true}; } catch (error) { return { - success: false, data: { user: {}, }, error: `Failed to get user profile: ${error instanceof Error ? error.message : String(error)}`, + success: false, }; } }; diff --git a/packages/nextjs/src/server/asgardeo.ts b/packages/nextjs/src/server/asgardeo.ts index 7605a2865..348dca150 100644 --- a/packages/nextjs/src/server/asgardeo.ts +++ b/packages/nextjs/src/server/asgardeo.ts @@ -16,35 +16,41 @@ * under the License. */ -import {TokenExchangeRequestConfig} from '@asgardeo/node'; -import AsgardeoNextClient from '../AsgardeoNextClient'; +import {TokenExchangeRequestConfig, TokenResponse} from '@asgardeo/node'; import getSessionIdAction from './actions/getSessionId'; +import AsgardeoNextClient from '../AsgardeoNextClient'; import {AsgardeoNextConfig} from '../models/config'; -const asgardeo = async () => { - const getAccessToken = async (sessionId: string) => { +const asgardeo = async (): Promise<{ + exchangeToken: (config: TokenExchangeRequestConfig, sessionId: string) => Promise; + getAccessToken: (sessionId: string) => Promise; + getSessionId: () => Promise; + reInitialize: (config: Partial) => Promise; +}> => { + const getAccessToken = async (sessionId: string): Promise => { const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); - return await client.getAccessToken(sessionId); + return client.getAccessToken(sessionId); }; - const getSessionId = async () => { - return await getSessionIdAction(); - }; + const getSessionId = async (): Promise => getSessionIdAction(); - const exchangeToken = async (config: TokenExchangeRequestConfig, sessionId: string) => { + const exchangeToken = async ( + config: TokenExchangeRequestConfig, + sessionId: string, + ): Promise => { const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); - return await client.exchangeToken(config, sessionId); + return client.exchangeToken(config, sessionId); }; - const reInitialize = async (config: Partial) => { + const reInitialize = async (config: Partial): Promise => { const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); - return await client.reInitialize(config); + return client.reInitialize(config); }; return { + exchangeToken, getAccessToken, getSessionId, - exchangeToken, reInitialize, }; }; diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 67d03be44..7fca31529 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -20,11 +20,10 @@ export {default as asgardeo} from './asgardeo'; -export {default as AsgardeoProvider} from './AsgardeoProvider'; -export * from './AsgardeoProvider'; +export {default as AsgardeoProvider} from './AsgardeoProvider.js'; +export * from './AsgardeoProvider.js'; export {default as asgardeoMiddleware} from './middleware/asgardeoMiddleware'; export * from './middleware/asgardeoMiddleware'; export {default as createRouteMatcher} from './middleware/createRouteMatcher'; -export * from './middleware/createRouteMatcher'; diff --git a/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts b/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts index c7d7a5c86..da9e33bcb 100644 --- a/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts +++ b/packages/nextjs/src/server/middleware/asgardeoMiddleware.ts @@ -17,7 +17,6 @@ */ import {NextRequest, NextResponse} from 'next/server'; -import {CookieConfig} from '@asgardeo/node'; import {AsgardeoNextConfig} from '../../models/config'; import SessionManager, {SessionTokenPayload} from '../../utils/SessionManager'; import { @@ -29,6 +28,12 @@ import { export type AsgardeoMiddlewareOptions = Partial; export type AsgardeoMiddlewareContext = { + /** Get the session payload from JWT session if available */ + getSession: () => Promise; + /** Get the session ID from the current request */ + getSessionId: () => string | undefined; + /** Check if the current request has a valid Asgardeo session */ + isSignedIn: () => boolean; /** * Protect a route by redirecting unauthenticated users. * Redirect URL fallback order: @@ -38,13 +43,7 @@ export type AsgardeoMiddlewareContext = { * 4. referer (if from same origin) * If none are available, throws an error. */ - protectRoute: (options?: {redirect?: string}) => Promise; - /** Check if the current request has a valid Asgardeo session */ - isSignedIn: () => boolean; - /** Get the session ID from the current request */ - getSessionId: () => string | undefined; - /** Get the session payload from JWT session if available */ - getSession: () => Promise; + protectRoute: (routeOptions?: {redirect?: string}) => Promise; }; type AsgardeoMiddlewareHandler = ( @@ -73,9 +72,8 @@ const hasValidSession = async (request: NextRequest): Promise => { * @param request - The Next.js request object * @returns The session ID if it exists, undefined otherwise */ -const getSessionIdFromRequestMiddleware = async (request: NextRequest): Promise => { - return await getSessionIdFromRequest(request); -}; +const getSessionIdFromRequestMiddleware = async (request: NextRequest): Promise => + getSessionIdFromRequest(request); /** * Asgardeo middleware that integrates authentication into your Next.js application. @@ -133,12 +131,13 @@ const getSessionIdFromRequestMiddleware = async (request: NextRequest): Promise< * }); * ``` */ -const asgardeoMiddleware = ( - handler?: AsgardeoMiddlewareHandler, - options?: AsgardeoMiddlewareOptions | ((req: NextRequest) => AsgardeoMiddlewareOptions), -): ((request: NextRequest) => Promise) => { - return async (request: NextRequest): Promise => { - const resolvedOptions = typeof options === 'function' ? options(request) : options || {}; +const asgardeoMiddleware = + ( + handler?: AsgardeoMiddlewareHandler, + options?: AsgardeoMiddlewareOptions | ((req: NextRequest) => AsgardeoMiddlewareOptions), + ): ((request: NextRequest) => Promise) => + async (request: NextRequest): Promise => { + const resolvedOptions: AsgardeoMiddlewareOptions = typeof options === 'function' ? options(request) : options || {}; const url: URL = new URL(request.url); const hasCallbackParams: boolean = url.searchParams.has('code') && url.searchParams.has('state'); @@ -166,27 +165,37 @@ const asgardeoMiddleware = ( } } - const sessionId = await getSessionIdFromRequestMiddleware(request); - const isAuthenticated = await hasValidSession(request); + const sessionId: string | undefined = await getSessionIdFromRequestMiddleware(request); + const isAuthenticated: boolean = await hasValidSession(request); const asgardeo: AsgardeoMiddlewareContext = { - protectRoute: async (options?: {redirect?: string}): Promise => { + getSession: async (): Promise => { + try { + return await getSessionFromRequest(request); + } catch { + return undefined; + } + }, + getSessionId: (): string | undefined => sessionId, + isSignedIn: (): boolean => isAuthenticated, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protectRoute: async (routeOptions?: {redirect?: string}): Promise => { // Skip protection if this is a validated OAuth callback - let the callback handler process it first // This prevents race conditions where middleware redirects before OAuth callback completes if (isValidOAuthCallback) { - return; + return undefined; } if (!isAuthenticated) { - const referer = request.headers.get('referer'); + const referer: string | null = request.headers.get('referer'); // TODO: Make this configurable or call the signIn() from here. let fallbackRedirect: string = '/'; // If referer exists and is from the same origin, use it as fallback if (referer) { try { - const refererUrl = new URL(referer); - const requestUrl = new URL(request.url); + const refererUrl: URL = new URL(referer); + const requestUrl: URL = new URL(request.url); if (refererUrl.origin === requestUrl.origin) { fallbackRedirect = refererUrl.pathname + refererUrl.search; @@ -199,27 +208,17 @@ const asgardeoMiddleware = ( // Fallback chain: options.redirect -> resolvedOptions.signInUrl -> resolvedOptions.defaultRedirect -> referer (same origin only) const redirectUrl: string = (resolvedOptions?.signInUrl as string) || fallbackRedirect; - const signInUrl = new URL(redirectUrl, request.url); + const signInUrl: URL = new URL(redirectUrl, request.url); return NextResponse.redirect(signInUrl); } - // Session exists, allow access - return; - }, - isSignedIn: () => isAuthenticated, - getSessionId: () => sessionId, - getSession: async () => { - try { - return await getSessionFromRequest(request); - } catch { - return undefined; - } + return undefined; }, }; if (handler) { - const result = await handler(asgardeo, request); + const result: NextResponse | void = await handler(asgardeo, request); if (result) { return result; } @@ -227,6 +226,5 @@ const asgardeoMiddleware = ( return NextResponse.next(); }; -}; export default asgardeoMiddleware; diff --git a/packages/nextjs/src/server/middleware/createRouteMatcher.ts b/packages/nextjs/src/server/middleware/createRouteMatcher.ts index a55eb34f7..0854012c8 100644 --- a/packages/nextjs/src/server/middleware/createRouteMatcher.ts +++ b/packages/nextjs/src/server/middleware/createRouteMatcher.ts @@ -37,20 +37,20 @@ import {NextRequest} from 'next/server'; * } * ``` */ -const createRouteMatcher = (patterns: string[]) => { - const regexPatterns = patterns.map(pattern => { +const createRouteMatcher = (patterns: string[]): ((req: NextRequest) => boolean) => { + const regexPatterns: RegExp[] = patterns.map((pattern: string): RegExp => { // Convert glob-like patterns to regex - const regexPattern = pattern - .replace(/\./g, '\\.') // Escape dots - .replace(/\*/g, '.*') // Convert * to .* + const regexPattern: string = pattern + .replace(/\./g, '\\.') // Escape dots + .replace(/\*/g, '.*') // Convert * to .* .replace(/\(\.\*\)/g, '(.*)'); // Handle explicit (.*) patterns return new RegExp(`^${regexPattern}$`); }); return (req: NextRequest): boolean => { - const pathname = req.nextUrl.pathname; - return regexPatterns.some(regex => regex.test(pathname)); + const {pathname} = req.nextUrl; + return regexPatterns.some((regex: RegExp): boolean => regex.test(pathname)); }; }; diff --git a/packages/nextjs/src/utils/SessionManager.ts b/packages/nextjs/src/utils/SessionManager.ts index 63a43c773..a83cceb09 100644 --- a/packages/nextjs/src/utils/SessionManager.ts +++ b/packages/nextjs/src/utils/SessionManager.ts @@ -16,39 +16,39 @@ * under the License. */ -import {SignJWT, jwtVerify, JWTPayload} from 'jose'; import {AsgardeoRuntimeError, CookieConfig} from '@asgardeo/node'; +import {SignJWT, jwtVerify, JWTPayload} from 'jose'; /** * Session token payload interface */ export interface SessionTokenPayload extends JWTPayload { - /** User ID */ - sub: string; - /** Session ID */ - sessionId: string; - /** OAuth scopes */ - scopes: string[]; - /** Organization ID if applicable */ - organizationId?: string; - /** Issued at timestamp */ - iat: number; /** Expiration timestamp */ exp: number; + /** Issued at timestamp */ + iat: number; + /** Organization ID if applicable */ + organizationId?: string; + /** OAuth scopes */ + scopes: string[]; + /** Session ID */ + sessionId: string; + /** User ID */ + sub: string; } /** * Session management utility class for JWT-based session cookies */ class SessionManager { - private static readonly DEFAULT_EXPIRY_SECONDS = 3600; + private static readonly DEFAULT_EXPIRY_SECONDS: number = 3600; /** * Get the signing secret from environment variable * Throws error in production if not set */ private static getSecret(): Uint8Array { - const secret = process.env['ASGARDEO_SECRET']; + const secret: string | undefined = process.env['ASGARDEO_SECRET']; if (!secret) { if (process.env['NODE_ENV'] === 'production') { @@ -60,7 +60,8 @@ class SessionManager { ); } // Use a default secret for development (not secure) - console.warn('⚠️ Using default secret for development. Set ASGARDEO_SECRET for production!'); + // eslint-disable-next-line no-console + console.warn('Using default secret for development. Set ASGARDEO_SECRET for production!'); return new TextEncoder().encode('development-secret-not-for-production'); } @@ -71,9 +72,9 @@ class SessionManager { * Create a temporary session cookie for login initiation */ static async createTempSession(sessionId: string): Promise { - const secret = this.getSecret(); + const secret: Uint8Array = this.getSecret(); - const jwt = await new SignJWT({ + const jwt: string = await new SignJWT({ sessionId, type: 'temp', }) @@ -96,13 +97,13 @@ class SessionManager { organizationId?: string, expirySeconds: number = this.DEFAULT_EXPIRY_SECONDS, ): Promise { - const secret = this.getSecret(); + const secret: Uint8Array = this.getSecret(); - const jwt = await new SignJWT({ + const jwt: string = await new SignJWT({ accessToken, - sessionId, - scopes, organizationId, + scopes, + sessionId, type: 'session', } as Omit) .setProtectedHeader({alg: 'HS256'}) @@ -119,7 +120,7 @@ class SessionManager { */ static async verifySessionToken(token: string): Promise { try { - const secret = this.getSecret(); + const secret: Uint8Array = this.getSecret(); const {payload} = await jwtVerify(token, secret); return payload as SessionTokenPayload; @@ -138,7 +139,7 @@ class SessionManager { */ static async verifyTempSession(token: string): Promise<{sessionId: string}> { try { - const secret = this.getSecret(); + const secret: Uint8Array = this.getSecret(); const {payload} = await jwtVerify(token, secret); if (payload['type'] !== 'temp') { @@ -159,26 +160,38 @@ class SessionManager { /** * Get session cookie options */ - static getSessionCookieOptions() { + static getSessionCookieOptions(): { + httpOnly: boolean; + maxAge: number; + path: string; + sameSite: 'lax'; + secure: boolean; + } { return { httpOnly: true, - secure: process.env['NODE_ENV'] === 'production', - sameSite: 'lax' as const, - path: '/', maxAge: this.DEFAULT_EXPIRY_SECONDS, + path: '/', + sameSite: 'lax' as const, + secure: process.env['NODE_ENV'] === 'production', }; } /** * Get temporary session cookie options */ - static getTempSessionCookieOptions() { + static getTempSessionCookieOptions(): { + httpOnly: boolean; + maxAge: number; + path: string; + sameSite: 'lax'; + secure: boolean; + } { return { httpOnly: true, - secure: process.env['NODE_ENV'] === 'production', - sameSite: 'lax' as const, - path: '/', maxAge: 15 * 60, + path: '/', + sameSite: 'lax' as const, + secure: process.env['NODE_ENV'] === 'production', }; } diff --git a/packages/nextjs/src/utils/createRouteMatcher.ts b/packages/nextjs/src/utils/createRouteMatcher.ts index 7af031fa8..9acad254f 100644 --- a/packages/nextjs/src/utils/createRouteMatcher.ts +++ b/packages/nextjs/src/utils/createRouteMatcher.ts @@ -37,10 +37,10 @@ import {NextRequest} from 'next/server'; * } * ``` */ -export const createRouteMatcher = (patterns: string[]) => { - const regexPatterns = patterns.map(pattern => { +export const createRouteMatcher = (patterns: string[]): ((req: NextRequest) => boolean) => { + const regexPatterns: RegExp[] = patterns.map((pattern: string) => { // Convert glob-like patterns to regex - const regexPattern = pattern + const regexPattern: string = pattern .replace(/\./g, '\\.') // Escape dots .replace(/\*/g, '.*') // Convert * to .* .replace(/\(\.\*\)/g, '(.*)'); // Handle explicit (.*) patterns @@ -49,7 +49,7 @@ export const createRouteMatcher = (patterns: string[]) => { }); return (req: NextRequest): boolean => { - const pathname = req.nextUrl.pathname; - return regexPatterns.some(regex => regex.test(pathname)); + const {pathname} = req.nextUrl; + return regexPatterns.some((regex: RegExp) => regex.test(pathname)); }; }; diff --git a/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts b/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts index e164d08af..528898704 100644 --- a/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts +++ b/packages/nextjs/src/utils/decorateConfigWithNextEnv.ts @@ -19,19 +19,31 @@ import {AsgardeoNextConfig} from '../models/config'; const decorateConfigWithNextEnv = (config: AsgardeoNextConfig): AsgardeoNextConfig => { - const {organizationHandle, scopes, applicationId, baseUrl, clientId, clientSecret, signInUrl, signUpUrl, afterSignInUrl, afterSignOutUrl, ...rest} = config; + const { + organizationHandle, + scopes, + applicationId, + baseUrl, + clientId, + clientSecret, + signInUrl, + signUpUrl, + afterSignInUrl, + afterSignOutUrl, + ...rest + } = config; return { ...rest, - scopes: scopes || (process.env['NEXT_PUBLIC_ASGARDEO_SCOPES'] as string), - organizationHandle: organizationHandle || (process.env['NEXT_PUBLIC_ASGARDEO_ORGANIZATION_HANDLE'] as string), + afterSignInUrl: afterSignInUrl || (process.env['NEXT_PUBLIC_ASGARDEO_AFTER_SIGN_IN_URL'] as string), + afterSignOutUrl: afterSignOutUrl || (process.env['NEXT_PUBLIC_ASGARDEO_AFTER_SIGN_OUT_URL'] as string), applicationId: applicationId || (process.env['NEXT_PUBLIC_ASGARDEO_APPLICATION_ID'] as string), baseUrl: baseUrl || (process.env['NEXT_PUBLIC_ASGARDEO_BASE_URL'] as string), clientId: clientId || (process.env['NEXT_PUBLIC_ASGARDEO_CLIENT_ID'] as string), clientSecret: clientSecret || (process.env['ASGARDEO_CLIENT_SECRET'] as string), - afterSignInUrl: afterSignInUrl || (process.env['NEXT_PUBLIC_ASGARDEO_AFTER_SIGN_IN_URL'] as string), + organizationHandle: organizationHandle || (process.env['NEXT_PUBLIC_ASGARDEO_ORGANIZATION_HANDLE'] as string), + scopes: scopes || (process.env['NEXT_PUBLIC_ASGARDEO_SCOPES'] as string), signInUrl: signInUrl || (process.env['NEXT_PUBLIC_ASGARDEO_SIGN_IN_URL'] as string), - afterSignOutUrl: afterSignOutUrl || (process.env['NEXT_PUBLIC_ASGARDEO_AFTER_SIGN_OUT_URL'] as string), signUpUrl: signUpUrl || (process.env['NEXT_PUBLIC_ASGARDEO_SIGN_UP_URL'] as string), }; }; diff --git a/packages/nextjs/src/utils/sessionUtils.ts b/packages/nextjs/src/utils/sessionUtils.ts index d4e93bf2b..2aa572173 100644 --- a/packages/nextjs/src/utils/sessionUtils.ts +++ b/packages/nextjs/src/utils/sessionUtils.ts @@ -18,7 +18,6 @@ import {NextRequest} from 'next/server'; import SessionManager, {SessionTokenPayload} from './SessionManager'; -import {CookieConfig} from '@asgardeo/node'; /** * Checks if a request has a valid session cookie (JWT). @@ -29,7 +28,7 @@ import {CookieConfig} from '@asgardeo/node'; */ export const hasValidSession = async (request: NextRequest): Promise => { try { - const sessionToken = request.cookies.get(SessionManager.getSessionCookieName())?.value; + const sessionToken: string | undefined = request.cookies.get(SessionManager.getSessionCookieName())?.value; if (!sessionToken) { return false; } @@ -50,7 +49,7 @@ export const hasValidSession = async (request: NextRequest): Promise => */ export const getSessionFromRequest = async (request: NextRequest): Promise => { try { - const sessionToken = request.cookies.get(SessionManager.getSessionCookieName())?.value; + const sessionToken: string | undefined = request.cookies.get(SessionManager.getSessionCookieName())?.value; if (!sessionToken) { return undefined; } @@ -70,13 +69,13 @@ export const getSessionFromRequest = async (request: NextRequest): Promise => { try { - const sessionPayload = await getSessionFromRequest(request); + const sessionPayload: SessionTokenPayload | undefined = await getSessionFromRequest(request); if (sessionPayload) { return sessionPayload.sessionId; } - return Promise.resolve(undefined); + return await Promise.resolve(undefined); } catch { return Promise.resolve(undefined); } @@ -90,12 +89,12 @@ export const getSessionIdFromRequest = async (request: NextRequest): Promise => { try { - const tempToken = request.cookies.get(SessionManager.getTempSessionCookieName())?.value; + const tempToken: string | undefined = request.cookies.get(SessionManager.getTempSessionCookieName())?.value; if (!tempToken) { return undefined; } - const tempSession = await SessionManager.verifyTempSession(tempToken); + const tempSession: {sessionId: string} = await SessionManager.verifyTempSession(tempToken); return tempSession.sessionId; } catch { return undefined; diff --git a/packages/nextjs/tsconfig.eslint.json b/packages/nextjs/tsconfig.eslint.json index 23fadc266..b03cf0d51 100644 --- a/packages/nextjs/tsconfig.eslint.json +++ b/packages/nextjs/tsconfig.eslint.json @@ -4,8 +4,10 @@ "**/.*.js", "**/.*.cjs", "**/.*.ts", + "**/.*.tsx", "**/*.js", "**/*.cjs", "**/*.ts", + "**/*.tsx", ] } diff --git a/packages/nextjs/vitest.config.ts b/packages/nextjs/vitest.config.ts index 29a917d10..31c7e5564 100644 --- a/packages/nextjs/vitest.config.ts +++ b/packages/nextjs/vitest.config.ts @@ -16,6 +16,7 @@ * under the License. */ +// eslint-disable-next-line import/no-extraneous-dependencies import {defineConfig} from 'vitest/config'; export default defineConfig({ diff --git a/packages/node/src/__legacy__/core/authentication.ts b/packages/node/src/__legacy__/core/authentication.ts index 28174b848..f5edfebab 100644 --- a/packages/node/src/__legacy__/core/authentication.ts +++ b/packages/node/src/__legacy__/core/authentication.ts @@ -175,22 +175,22 @@ export class AsgardeoNodeCore { public async isSignedIn(userId: string): Promise { try { if (!(await this.auth.isSignedIn(userId))) { - return Promise.resolve(false); + return await Promise.resolve(false); } if (await SessionUtils.validateSession(await this.storageManager.getSessionData(userId))) { - return Promise.resolve(true); + return await Promise.resolve(true); } const refreshedToken: TokenResponse = await this.refreshAccessToken(userId); if (refreshedToken) { - return Promise.resolve(true); + return await Promise.resolve(true); } this.storageManager.removeSessionData(userId); this.storageManager.getTemporaryData(userId); - return Promise.resolve(false); + return await Promise.resolve(false); } catch (error) { return Promise.reject(error); } diff --git a/packages/react/.eslintrc.cjs b/packages/react/.eslintrc.cjs index 2676ca816..a5734f0d4 100644 --- a/packages/react/.eslintrc.cjs +++ b/packages/react/.eslintrc.cjs @@ -35,4 +35,11 @@ module.exports = { project: [path.resolve(__dirname, 'tsconfig.eslint.json')], }, plugins: ['@wso2'], + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx', '.d.ts', '.mjs', '.json'], + }, + }, + }, }; diff --git a/packages/react/esbuild.config.mjs b/packages/react/esbuild.config.mjs index efecbbeb8..edbe35e92 100644 --- a/packages/react/esbuild.config.mjs +++ b/packages/react/esbuild.config.mjs @@ -31,8 +31,8 @@ const commonOptions = { plugins: [ preserveDirectivesPlugin({ directives: ['use client', 'use strict'], - include: /\.(js|ts|jsx|tsx)$/, exclude: /node_modules/, + include: /\.(js|ts|jsx|tsx)$/, }), ], target: ['es2020'], diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index ca88370ac..ac4de3e74 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -42,7 +42,6 @@ import { TokenResponse, HttpRequestConfig, HttpResponse, - Storage, navigate, getRedirectBasedSignUpUrl, Config, @@ -54,11 +53,11 @@ import { EmbeddedSignInFlowStatusV2, } from '@asgardeo/browser'; import AuthAPI from './__temp__/api'; +import getAllOrganizations from './api/getAllOrganizations'; import getMeOrganizations from './api/getMeOrganizations'; -import getScim2Me from './api/getScim2Me'; import getSchemas from './api/getSchemas'; +import getScim2Me from './api/getScim2Me'; import {AsgardeoReactConfig} from './models/config'; -import getAllOrganizations from './api/getAllOrganizations'; /** * Client for mplementing Asgardeo in React applications. @@ -68,8 +67,10 @@ import getAllOrganizations from './api/getAllOrganizations'; */ class AsgardeoReactClient extends AsgardeoBrowserClient { private asgardeo: AuthAPI; - private _isLoading: boolean = false; - private _instanceId: number; + + private loadingState: boolean = false; + + private clientInstanceId: number; /** * Creates a new AsgardeoReactClient instance. @@ -77,7 +78,7 @@ class AsgardeoReactClient e */ constructor(instanceId: number = 0) { super(); - this._instanceId = instanceId; + this.clientInstanceId = instanceId; // FIXME: This has to be the browser client from `@asgardeo/browser` package. this.asgardeo = new AuthAPI(undefined, instanceId); @@ -88,7 +89,7 @@ class AsgardeoReactClient e * @returns The instance ID used for multi-auth context support. */ public getInstanceId(): number { - return this._instanceId; + return this.clientInstanceId; } /** @@ -96,7 +97,7 @@ class AsgardeoReactClient e * @param loading - Boolean indicating if the client is in a loading state */ private setLoading(loading: boolean): void { - this._isLoading = loading; + this.loadingState = loading; } /** @@ -104,26 +105,26 @@ class AsgardeoReactClient e * @param operation - The async operation to execute * @returns Promise with the result of the operation */ - private async withLoading(operation: () => Promise): Promise { + private async withLoading(operation: () => Promise): Promise { this.setLoading(true); try { - const result = await operation(); + const result: TResult = await operation(); return result; } finally { this.setLoading(false); } } - override initialize(config: AsgardeoReactConfig, storage?: Storage): Promise { + override initialize(config: AsgardeoReactConfig): Promise { let resolvedOrganizationHandle: string | undefined = config?.organizationHandle; if (!resolvedOrganizationHandle) { resolvedOrganizationHandle = deriveOrganizationHandleFromBaseUrl(config?.baseUrl); } - return this.withLoading(async () => { - return this.asgardeo.init({...config, organizationHandle: resolvedOrganizationHandle} as any); - }); + return this.withLoading(async () => + this.asgardeo.init({...config, organizationHandle: resolvedOrganizationHandle} as any), + ); } override reInitialize(config: Partial): Promise { @@ -147,21 +148,22 @@ class AsgardeoReactClient e }); } - override async updateUserProfile(payload: any, userId?: string): Promise { + // 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 = options?.baseUrl; + let baseUrl: string = options?.baseUrl; if (!baseUrl) { - const configData = await this.asgardeo.getConfigData(); + const configData: any = await this.asgardeo.getConfigData(); baseUrl = configData?.baseUrl; } - const profile = await getScim2Me({baseUrl}); - const schemas = await getSchemas({baseUrl}); + const profile: User = await getScim2Me({baseUrl}); + const schemas: any = await getSchemas({baseUrl}); return generateUserProfile(profile, flattenUserSchema(schemas)); } catch (error) { @@ -174,53 +176,51 @@ class AsgardeoReactClient e } async getIdToken(): Promise { - return this.withLoading(async () => { - return this.asgardeo.getIdToken(); - }); + return this.withLoading(async () => this.asgardeo.getIdToken()); } async getUserProfile(options?: any): Promise { return this.withLoading(async () => { try { - let baseUrl = options?.baseUrl; + let baseUrl: string = options?.baseUrl; if (!baseUrl) { - const configData = await this.asgardeo.getConfigData(); + const configData: any = await this.asgardeo.getConfigData(); baseUrl = configData?.baseUrl; } - const profile = await getScim2Me({baseUrl}); - const schemas = await getSchemas({baseUrl}); + const profile: User = await getScim2Me({baseUrl}); + const schemas: any = await getSchemas({baseUrl}); - const processedSchemas = flattenUserSchema(schemas); + const processedSchemas: any = flattenUserSchema(schemas); - const output = { - schemas: processedSchemas, + const output: UserProfile = { flattenedProfile: generateFlattenedUserProfile(profile, processedSchemas), profile, + schemas: processedSchemas, }; return output; } catch (error) { return { - schemas: [], flattenedProfile: extractUserClaimsFromIdToken(await this.getDecodedIdToken()), profile: extractUserClaimsFromIdToken(await this.getDecodedIdToken()), + schemas: [], }; } }); } - override async getMyOrganizations(options?: any, sessionId?: string): Promise { + override async getMyOrganizations(options?: any): Promise { try { - let baseUrl = options?.baseUrl; + let baseUrl: string = options?.baseUrl; if (!baseUrl) { - const configData = await this.asgardeo.getConfigData(); + const configData: any = await this.asgardeo.getConfigData(); baseUrl = configData?.baseUrl; } - return getMeOrganizations({baseUrl}); + return await getMeOrganizations({baseUrl}); } catch (error) { throw new AsgardeoRuntimeError( `Failed to fetch the user's associated organizations: ${ @@ -233,16 +233,16 @@ class AsgardeoReactClient e } } - override async getAllOrganizations(options?: any, sessionId?: string): Promise { + override async getAllOrganizations(options?: any): Promise { try { - let baseUrl = options?.baseUrl; + let baseUrl: string = options?.baseUrl; if (!baseUrl) { - const configData = await this.asgardeo.getConfigData(); + const configData: any = await this.asgardeo.getConfigData(); baseUrl = configData?.baseUrl; } - return getAllOrganizations({baseUrl}); + return await getAllOrganizations({baseUrl}); } catch (error) { throw new AsgardeoRuntimeError( `Failed to fetch all organizations: ${error instanceof Error ? error.message : String(error)}`, @@ -255,12 +255,12 @@ class AsgardeoReactClient e override async getCurrentOrganization(): Promise { try { - return this.withLoading(async () => { + return await this.withLoading(async () => { const idToken: IdToken = await this.getDecodedIdToken(); return { - orgHandle: idToken?.org_handle, - name: idToken?.org_name, id: idToken?.org_id, + name: idToken?.org_name, + orgHandle: idToken?.org_handle, }; }); } catch (error) { @@ -273,11 +273,9 @@ class AsgardeoReactClient e } } - override async switchOrganization(organization: Organization, sessionId?: string): Promise { + override async switchOrganization(organization: Organization): Promise { return this.withLoading(async () => { try { - const configData = await this.asgardeo.getConfigData(); - if (!organization.id) { throw new AsgardeoRuntimeError( 'Organization ID is required for switching organizations', @@ -287,7 +285,7 @@ class AsgardeoReactClient e ); } - const exchangeConfig = { + const exchangeConfig: TokenExchangeRequestConfig = { attachToken: false, data: { client_id: '{{clientId}}', @@ -301,7 +299,7 @@ class AsgardeoReactClient e signInRequired: true, }; - return (await this.asgardeo.exchangeToken(exchangeConfig, (user: User) => {})) as TokenResponse | Response; + return (await this.asgardeo.exchangeToken(exchangeConfig, () => {})) as TokenResponse | Response; } catch (error) { throw new AsgardeoRuntimeError( `Failed to switch organization: ${error.message || error}`, @@ -314,7 +312,7 @@ class AsgardeoReactClient e } override isLoading(): boolean { - return this._isLoading || this.asgardeo.isLoading(); + return this.loadingState || this.asgardeo.isLoading(); } async isInitialized(): Promise { @@ -322,20 +320,17 @@ class AsgardeoReactClient e } override async isSignedIn(): Promise { - return await this.asgardeo.isSignedIn(); + return this.asgardeo.isSignedIn(); } override getConfiguration(): T { return this.asgardeo.getConfigData() as unknown as T; } - override async exchangeToken( - config: TokenExchangeRequestConfig, - sessionId?: string, - ): Promise { - return this.withLoading(async () => { - return this.asgardeo.exchangeToken(config, (user: User) => {}) as unknown as TokenResponse | Response; - }); + override async exchangeToken(config: TokenExchangeRequestConfig): Promise { + return this.withLoading( + async () => this.asgardeo.exchangeToken(config, () => {}) as unknown as TokenResponse | Response, + ); } override signIn( @@ -351,15 +346,16 @@ class AsgardeoReactClient e ): Promise; override async signIn(...args: any[]): Promise { return this.withLoading(async () => { - const arg1 = args[0]; - const arg2 = args[1]; + const arg1: any = args[0]; + const arg2: any = args[1]; const config: AsgardeoReactConfig | undefined = (await this.asgardeo.getConfigData()) as | AsgardeoReactConfig | undefined; - const platformFromStorage = sessionStorage.getItem('asgardeo_platform'); - const isV2Platform = (config && config.platform === Platform.AsgardeoV2) || platformFromStorage === 'AsgardeoV2'; + 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; @@ -378,11 +374,11 @@ class AsgardeoReactClient e const baseUrlFromStorage: string = sessionStorage.getItem('asgardeo_base_url'); const baseUrl: string = config?.baseUrl || baseUrlFromStorage; - const response = await executeEmbeddedSignInFlowV2({ + const response: EmbeddedSignInFlowResponseV2 = await executeEmbeddedSignInFlowV2({ + authId, + baseUrl, payload: arg1 as EmbeddedSignInFlowHandleRequestPayload, url: arg2?.url, - baseUrl, - authId, }); /** @@ -390,7 +386,7 @@ class AsgardeoReactClient e * token), we manually set the session using that assertion. This is a temporary workaround until the platform * fully supports session management for embedded flows. * - * Tracker: + * Tracker: */ if ( isV2Platform && @@ -399,24 +395,29 @@ class AsgardeoReactClient e response['flowStatus'] === EmbeddedSignInFlowStatusV2.Complete && response['assertion'] ) { - const decodedAssertion = await this.decodeJwtToken<{ - iat?: number; + 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 = decodedAssertion.iat ? decodedAssertion.iat * 1000 : Date.now(); - const expiresIn = + 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'], - id_token: response['assertion'], - token_type: 'Bearer', - expires_in: expiresIn, created_at: createdAt, + expires_in: expiresIn, + id_token: response['assertion'], scope: decodedAssertion.scope, + token_type: 'Bearer', }); } @@ -469,7 +470,7 @@ class AsgardeoReactClient e override async signUp(payload: EmbeddedFlowExecuteRequestPayload): Promise; override async signUp(...args: any[]): Promise { const config: AsgardeoReactConfig = (await this.asgardeo.getConfigData()) as AsgardeoReactConfig; - const firstArg = args[0]; + const firstArg: any = args[0]; const baseUrl: string = config?.baseUrl; if (config.platform === Platform.AsgardeoV2) { @@ -484,8 +485,8 @@ class AsgardeoReactClient e } return executeEmbeddedSignUpFlowV2({ - baseUrl, authId, + baseUrl, payload: typeof firstArg === 'object' && 'flowType' in firstArg ? {...(firstArg as EmbeddedFlowExecuteRequestPayload), verbose: true} @@ -501,6 +502,7 @@ class AsgardeoReactClient e } navigate(getRedirectBasedSignUpUrl(config as Config)); + return undefined; } async request(requestConfig?: HttpRequestConfig): Promise> { @@ -520,11 +522,11 @@ class AsgardeoReactClient e } override async setSession(sessionData: Record, sessionId?: string): Promise { - return await (await this.asgardeo.getStorageManager()).setSessionData(sessionData, sessionId); + return (await this.asgardeo.getStorageManager()).setSessionData(sessionData, sessionId); } - override decodeJwtToken>(token: string): Promise { - return this.asgardeo.decodeJwtToken(token); + override decodeJwtToken>(token: string): Promise { + return this.asgardeo.decodeJwtToken(token); } } diff --git a/packages/react/src/__temp__/api.ts b/packages/react/src/__temp__/api.ts index 9edde4559..dc0474f89 100644 --- a/packages/react/src/__temp__/api.ts +++ b/packages/react/src/__temp__/api.ts @@ -35,15 +35,17 @@ import {AuthStateInterface} from './models'; class AuthAPI { static DEFAULT_STATE: AuthStateInterface; - private _authState = AuthAPI.DEFAULT_STATE; - private _client: AsgardeoSPAClient; - private _instanceId: number; + private authState: AuthStateInterface = AuthAPI.DEFAULT_STATE; - private _isLoading: boolean; + private client: AsgardeoSPAClient; + + private apiInstanceId: number; + + private loadingState: boolean; constructor(spaClient?: AsgardeoSPAClient, instanceId: number = 0) { - this._instanceId = instanceId; - this._client = spaClient ?? AsgardeoSPAClient.getInstance(instanceId); + this.apiInstanceId = instanceId; + this.client = spaClient ?? AsgardeoSPAClient.getInstance(instanceId); this.getState = this.getState.bind(this); this.init = this.init.bind(this); @@ -57,19 +59,19 @@ class AuthAPI { * @returns The instance ID used for multi-auth context support. */ public getInstanceId(): number { - return this._instanceId; + return this.apiInstanceId; } - public _setIsLoading(isLoading: boolean): void { - this._isLoading = isLoading; + public setLoadingState(isLoading: boolean): void { + this.loadingState = isLoading; } - public _getIsLoading(): boolean { - return this._isLoading; + public getLoadingState(): boolean { + return this.loadingState; } public isLoading(): boolean { - return this._getIsLoading(); + return this.getLoadingState(); } /** @@ -78,7 +80,7 @@ class AuthAPI { * @return {AuthStateInterface} Authentication State. */ public getState(): AuthStateInterface { - return this._authState; + return this.authState; } /** @@ -87,7 +89,7 @@ class AuthAPI { * @param {Config} config - `dispatch` function from React Auth Context. */ public async init(config: AuthClientConfig): Promise { - return this._client.initialize(config); + return this.client.initialize(config); } /** @@ -96,11 +98,11 @@ class AuthAPI { * @returns {Promise>} - A promise that resolves with the configuration data. */ public async getConfigData(): Promise> { - return this._client.getConfigData(); + return this.client.getConfigData(); } public async getStorageManager(): Promise { - return this._client.getStorageManager(); + return this.client.getStorageManager(); } /** @@ -110,7 +112,7 @@ class AuthAPI { */ public async isInitialized(): Promise { // Wait for initialization to complete - return this._client.isInitialized(); + return this.client.isInitialized(); } /** @@ -132,27 +134,26 @@ class AuthAPI { params: Record; }, ): Promise { - return this._client + return this.client .signIn(config, authorizationCode, sessionState, authState, tokenRequestConfig) .then(async (response: User) => { if (!response) { return null; // FIXME: Validate this. Temp fix for: error TS7030: Not all code paths return a value. } - if (await this._client.isSignedIn()) { - const stateToUpdate = { + if (await this.client.isSignedIn()) { + const stateToUpdate: AuthStateInterface = { displayName: response.displayName, email: response.email, - isSignedIn: true, isLoading: false, - isSigningOut: false, + isSignedIn: true, username: response.username, }; this.updateState(stateToUpdate); // dispatch({...state, ...stateToUpdate}); - this._setIsLoading(false); + this.setLoadingState(false); if (callback) { callback(response); @@ -161,7 +162,7 @@ class AuthAPI { return response; }) - .catch(error => Promise.reject(error)); + .catch((error: Error) => Promise.reject(error)); } /** @@ -172,16 +173,16 @@ class AuthAPI { * @param {any} callback - Action to trigger on successful sign out. */ public signOut(callback?: (response?: boolean) => void): Promise { - return this._client + return this.client .signOut() - .then(response => { + .then((response: boolean) => { if (callback) { callback(response); } return response; }) - .catch(error => Promise.reject(error)); + .catch((error: Error) => Promise.reject(error)); } /** @@ -190,7 +191,7 @@ class AuthAPI { * @param {AuthStateInterface} state - State values to update in authentication state. */ public updateState(state: AuthStateInterface): void { - this._authState = {...this._authState, ...state}; + this.authState = {...this.authState, ...state}; } /** @@ -199,7 +200,7 @@ class AuthAPI { * @return {Promise} - A promise that resolves with the user information. */ public async getUser(): Promise { - return this._client.getUser(); + return this.client.getUser(); } /** @@ -213,7 +214,7 @@ class AuthAPI { * @return {Promise} - Returns a Promise that resolves with the response to the request. */ public async httpRequest(config: HttpRequestConfig): Promise> { - return this._client.httpRequest(config); + return this.client.httpRequest(config); } /** @@ -227,7 +228,7 @@ class AuthAPI { * @return {Promise} - Returns a Promise that resolves with the responses to the requests. */ public async httpRequestAll(configs: HttpRequestConfig[]): Promise[]> { - return this._client.httpRequestAll(configs); + return this.client.httpRequestAll(configs); } /** @@ -242,7 +243,7 @@ class AuthAPI { config: SPACustomGrantConfig, callback: (response: User | Response) => void, ): Promise { - return this._client + return this.client .exchangeToken(config) .then((response: User | Response) => { if (!response) { @@ -253,16 +254,18 @@ class AuthAPI { this.updateState({ ...this.getState(), ...(response as User), - isSignedIn: true, isLoading: false, + isSignedIn: true, }); } - callback && callback(response); + if (callback) { + callback(response); + } return response; }) - .catch(error => Promise.reject(error)); + .catch((error: Error) => Promise.reject(error)); } /** @@ -271,14 +274,14 @@ class AuthAPI { * @return {Promise} - A promise that resolves with `true` if the process is successful. */ public async revokeAccessToken(dispatch: (state: AuthStateInterface) => void): Promise { - return this._client + return this.client .revokeAccessToken() .then(() => { this.updateState({...AuthAPI.DEFAULT_STATE, isLoading: false}); dispatch(AuthAPI.DEFAULT_STATE); return true; }) - .catch(error => Promise.reject(error)); + .catch((error: Error) => Promise.reject(error)); } /** @@ -287,7 +290,7 @@ class AuthAPI { * @return {Promise { - return this._client.getOpenIDProviderEndpoints(); + return this.client.getOpenIDProviderEndpoints(); } /** @@ -296,7 +299,7 @@ class AuthAPI { * @return {HttpClientInstance} - The Axios HTTP client. */ public async getHttpClient(): Promise { - return this._client.getHttpClient(); + return this.client.getHttpClient(); } /** @@ -306,7 +309,7 @@ class AuthAPI { * @returns The decoded token payload. */ public async decodeJwtToken>(token: string): Promise { - return this._client.decodeJwtToken(token); + return this.client.decodeJwtToken(token); } /** @@ -316,7 +319,7 @@ class AuthAPI { * the decoded payload of the id token. */ public async getDecodedIdToken(sessionId?: string): Promise { - return this._client.getDecodedIdToken(sessionId); + return this.client.getDecodedIdToken(sessionId); } /** @@ -326,7 +329,7 @@ class AuthAPI { * the decoded payload of the idp id token. */ public async getDecodedIDPIDToken(): Promise { - return this._client.getDecodedIdToken(); + return this.client.getDecodedIdToken(); } /** @@ -335,7 +338,7 @@ class AuthAPI { * @return {Promise} - A Promise that resolves with the id token. */ public async getIdToken(): Promise { - return this._client.getIdToken(); + return this.client.getIdToken(); } /** @@ -346,7 +349,7 @@ class AuthAPI { * @return {Promise} - A Promise that resolves with the access token. */ public async getAccessToken(sessionId?: string): Promise { - return this._client.getAccessToken(sessionId); + return this.client.getAccessToken(sessionId); } /** @@ -358,7 +361,7 @@ class AuthAPI { * @return {Promise} - A Promise that resolves with the idp access token. */ public async getIDPAccessToken(): Promise { - return this._client.getIDPAccessToken(); + return this.client.getIDPAccessToken(); } /** @@ -368,7 +371,7 @@ class AuthAPI { * information about the refreshed access token. */ public async refreshAccessToken(): Promise { - return this._client.refreshAccessToken(); + return this.client.refreshAccessToken(); } /** @@ -377,7 +380,7 @@ class AuthAPI { * @return {Promise} - A Promise that resolves with `true` if teh user is authenticated. */ public async isSignedIn(): Promise { - return this._client.isSignedIn(); + return this.client.isSignedIn(); } /** @@ -386,7 +389,7 @@ class AuthAPI { * @return {Promise} - A Promise that resolves with `true` if there is an active session. */ public async isSessionActive(): Promise { - return this._client.isSessionActive(); + return this.client.isSessionActive(); } /** @@ -396,7 +399,7 @@ class AuthAPI { * */ public async enableHttpHandler(): Promise { - return this._client.enableHttpHandler(); + return this.client.enableHttpHandler(); } /** @@ -405,7 +408,7 @@ class AuthAPI { * @return {Promise} - A promise that resolves with True. */ public async disableHttpHandler(): Promise { - return this._client.disableHttpHandler(); + return this.client.disableHttpHandler(); } /** @@ -414,7 +417,7 @@ class AuthAPI { * @param {Partial>} config - A config object to update the SDK configurations with. */ public async reInitialize(config: Partial>): Promise { - return this._client.reInitialize(config); + return this.client.reInitialize(config); } /** @@ -429,10 +432,10 @@ class AuthAPI { 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, id); } - return this._client.on(hook, callback); + return this.client.on(hook, callback); } /** @@ -452,7 +455,7 @@ class AuthAPI { additionalParams?: Record, tokenRequestConfig?: {params: Record}, ): Promise { - return this._client + return this.client .signInSilently(additionalParams, tokenRequestConfig) .then(async (response: User | boolean) => { if (!response) { @@ -461,7 +464,7 @@ class AuthAPI { return response; }) - .catch(error => Promise.reject(error)); + .catch((error: Error) => Promise.reject(error)); } /** @@ -472,15 +475,15 @@ class AuthAPI { * @return void */ public clearSession(sessionId?: string): void { - this._client.clearSession(sessionId); + this.client.clearSession(sessionId); } } AuthAPI.DEFAULT_STATE = { displayName: '', email: '', - isSignedIn: false, isLoading: true, + isSignedIn: false, username: '', }; diff --git a/packages/react/src/__temp__/models.ts b/packages/react/src/__temp__/models.ts index e188a67be..7341167aa 100644 --- a/packages/react/src/__temp__/models.ts +++ b/packages/react/src/__temp__/models.ts @@ -1,7 +1,7 @@ /** - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * 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 @@ -33,18 +33,18 @@ import { } from '@asgardeo/browser'; export interface ReactConfig { + disableAutoSignIn?: boolean; + /** + * The `AuthProvider`, by default, looks for an active session in the server and updates the session information + * with the latest session information from the server. This option could be used to disable that behaviour. + */ + disableTrySignInSilently?: boolean; /** * The SDK's `AuthProvider` by default is listening to the URL changes to see * if `code` & `session_state` search params are available so that it could perform * token exchange. This option could be used to override that behaviour. */ skipRedirectCallback?: boolean; - /** - * The `AuthProvider`, by default, looks for an active session in the server and updates the session information - * with the latest session information from the server. This option could be used to disable that behaviour. - */ - disableTrySignInSilently?: boolean; - disableAutoSignIn?: boolean; } export type AuthReactConfig = AuthSPAClientConfig & ReactConfig; @@ -62,14 +62,14 @@ export interface AuthStateInterface { * The email address of the user. */ email?: string; - /** - * Specifies if the user is authenticated or not. - */ - isSignedIn: boolean; /** * Are the Auth requests loading. */ isLoading: boolean; + /** + * Specifies if the user is authenticated or not. + */ + isSignedIn: boolean; /** * The username of the user. */ @@ -77,6 +77,26 @@ export interface AuthStateInterface { } export interface AuthContextInterface { + disableHttpHandler(): Promise; + enableHttpHandler(): Promise; + error: AsgardeoAuthException; + exchangeToken(config: TokenExchangeRequestConfig, callback?: (response: User | Response) => void): void; + getAccessToken(): Promise; + getDecodedIDPIDToken(): Promise; + getDecodedIdToken(sessionId?: string): Promise; + getHttpClient(): Promise; + getIdToken(): Promise; + getOpenIDProviderEndpoints(): Promise; + getUser(): Promise; + httpRequest(config: HttpRequestConfig): Promise>; + httpRequestAll(configs: HttpRequestConfig[]): Promise[]>; + isSignedIn(): Promise; + on(hook: Exclude, callback: (response?: any) => void): void; + on(hook: Hooks.CustomGrant, callback: (response?: any) => void, id: string): void; + on(hook: Hooks, callback: (response?: any) => void, id?: string): void; + reInitialize(config: Partial>): Promise; + refreshAccessToken(): Promise; + revokeAccessToken(): Promise; signIn: ( config?: SignInConfig, authorizationCode?: string, @@ -87,32 +107,12 @@ export interface AuthContextInterface { params: Record; }, ) => Promise; - signOut: (callback?: (response: boolean) => void) => Promise; - getUser(): Promise; - httpRequest(config: HttpRequestConfig): Promise>; - httpRequestAll(configs: HttpRequestConfig[]): Promise[]>; - exchangeToken(config: TokenExchangeRequestConfig, callback?: (response: User | Response) => void): void; - revokeAccessToken(): Promise; - getOpenIDProviderEndpoints(): Promise; - getHttpClient(): Promise; - getDecodedIDPIDToken(): Promise; - getDecodedIdToken(sessionId?: string): Promise; - getIdToken(): Promise; - getAccessToken(): Promise; - refreshAccessToken(): Promise; - isSignedIn(): Promise; - enableHttpHandler(): Promise; - disableHttpHandler(): Promise; - reInitialize(config: Partial>): Promise; signInSilently: ( additionalParams?: Record, tokenRequestConfig?: {params: Record}, ) => Promise; - on(hook: Hooks.CustomGrant, callback: (response?: any) => void, id: string): void; - on(hook: Exclude, callback: (response?: any) => void): void; - on(hook: Hooks, callback: (response?: any) => void, id?: string): void; + signOut: (callback?: (response: boolean) => void) => Promise; state: AuthStateInterface; - error: AsgardeoAuthException; } /** diff --git a/packages/react/src/api/createOrganization.ts b/packages/react/src/api/createOrganization.ts index 8e0dfa748..ca8653aad 100644 --- a/packages/react/src/api/createOrganization.ts +++ b/packages/react/src/api/createOrganization.ts @@ -19,11 +19,11 @@ import { Organization, HttpInstance, + HttpResponse, AsgardeoSPAClient, HttpRequestConfig, createOrganization as baseCreateOrganization, CreateOrganizationConfig as BaseCreateOrganizationConfig, - CreateOrganizationPayload, } from '@asgardeo/browser'; const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); @@ -92,18 +92,18 @@ export interface CreateOrganizationConfig extends Omit => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const response = await httpClient({ - url, - method: config.method || 'POST', - headers: config.headers as Record, + const response: HttpResponse = await httpClient({ data: config.body ? JSON.parse(config.body as string) : undefined, + headers: config.headers as Record, + method: config.method || 'POST', + url, } as HttpRequestConfig); return { + json: () => Promise.resolve(response.data), ok: response.status >= 200 && response.status < 300, status: response.status, statusText: response.statusText || '', - json: () => Promise.resolve(response.data), text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), } as Response; }; diff --git a/packages/react/src/api/getAllOrganizations.ts b/packages/react/src/api/getAllOrganizations.ts index 48f79aa7a..8686016ae 100644 --- a/packages/react/src/api/getAllOrganizations.ts +++ b/packages/react/src/api/getAllOrganizations.ts @@ -18,6 +18,7 @@ import { HttpInstance, + HttpResponse, AsgardeoSPAClient, HttpRequestConfig, getAllOrganizations as baseGetAllOrganizations, @@ -86,17 +87,17 @@ const getAllOrganizations = async ({ ...requestConfig }: GetAllOrganizationsConfig): Promise => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const response = await httpClient({ - url, - method: config.method || 'GET', + 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 || '', - json: () => Promise.resolve(response.data), text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), } as Response; }; diff --git a/packages/react/src/api/getMeOrganizations.ts b/packages/react/src/api/getMeOrganizations.ts index 9ed9a56ed..ff8adb0fc 100644 --- a/packages/react/src/api/getMeOrganizations.ts +++ b/packages/react/src/api/getMeOrganizations.ts @@ -19,6 +19,7 @@ import { Organization, HttpInstance, + HttpResponse, AsgardeoSPAClient, HttpRequestConfig, getMeOrganizations as baseGetMeOrganizations, @@ -87,17 +88,17 @@ export interface GetMeOrganizationsConfig extends Omit => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const response = await httpClient({ - url, - method: config.method || 'GET', + 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 || '', - json: () => Promise.resolve(response.data), text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), } as Response; }; diff --git a/packages/react/src/api/getOrganization.ts b/packages/react/src/api/getOrganization.ts index 477415452..227707c2e 100644 --- a/packages/react/src/api/getOrganization.ts +++ b/packages/react/src/api/getOrganization.ts @@ -18,6 +18,7 @@ import { HttpInstance, + HttpResponse, AsgardeoSPAClient, HttpRequestConfig, getOrganization as baseGetOrganization, @@ -79,17 +80,17 @@ export interface GetOrganizationConfig extends Omit => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const response = await httpClient({ - url, - method: config.method || 'GET', + 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 || '', - json: () => Promise.resolve(response.data), text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), } as Response; }; diff --git a/packages/react/src/api/getSchemas.ts b/packages/react/src/api/getSchemas.ts index ff1e25a32..746a814b4 100644 --- a/packages/react/src/api/getSchemas.ts +++ b/packages/react/src/api/getSchemas.ts @@ -19,6 +19,7 @@ import { Schema, HttpInstance, + HttpResponse, AsgardeoSPAClient, HttpRequestConfig, getSchemas as baseGetSchemas, @@ -77,17 +78,17 @@ export interface GetSchemasConfig extends Omit */ const getSchemas = async ({fetcher, ...requestConfig}: GetSchemasConfig): Promise => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const response = await httpClient({ - url, - method: config.method || 'GET', + 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 || '', - json: () => Promise.resolve(response.data), text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), } as Response; }; diff --git a/packages/react/src/api/getScim2Me.ts b/packages/react/src/api/getScim2Me.ts index d5c5bb4c6..326d02a3b 100644 --- a/packages/react/src/api/getScim2Me.ts +++ b/packages/react/src/api/getScim2Me.ts @@ -18,12 +18,12 @@ import { User, - AsgardeoAPIError, HttpInstance, + HttpResponse, AsgardeoSPAClient, HttpRequestConfig, getScim2Me as baseGetScim2Me, - GetScim2MeConfig as BaseGetScim2MeConfig + GetScim2MeConfig as BaseGetScim2MeConfig, } from '@asgardeo/browser'; const httpClient: HttpInstance = AsgardeoSPAClient.getInstance().httpRequest.bind(AsgardeoSPAClient.getInstance()); @@ -78,17 +78,17 @@ export interface GetScim2MeConfig extends Omit */ const getScim2Me = async ({fetcher, ...requestConfig}: GetScim2MeConfig): Promise => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const response = await httpClient({ - url, - method: config.method || 'GET', + 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 || '', - json: () => Promise.resolve(response.data), text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), } as Response; }; diff --git a/packages/react/src/api/updateMeProfile.ts b/packages/react/src/api/updateMeProfile.ts index 61285a779..c14413851 100644 --- a/packages/react/src/api/updateMeProfile.ts +++ b/packages/react/src/api/updateMeProfile.ts @@ -19,6 +19,7 @@ import { User, HttpInstance, + HttpResponse, AsgardeoSPAClient, HttpRequestConfig, updateMeProfile as baseUpdateMeProfile, @@ -65,18 +66,18 @@ export interface UpdateMeProfileConfig extends Omit => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const response = await httpClient({ - url, - method: config.method || 'PATCH', - headers: config.headers as Record, + const response: HttpResponse = await httpClient({ data: config.body ? JSON.parse(config.body as string) : undefined, + headers: config.headers as Record, + method: config.method || 'PATCH', + url, } as HttpRequestConfig); return { + json: () => Promise.resolve(response.data), ok: response.status >= 200 && response.status < 300, status: response.status, statusText: response.statusText || '', - json: () => Promise.resolve(response.data), text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), } as Response; }; diff --git a/packages/react/src/api/updateOrganization.ts b/packages/react/src/api/updateOrganization.ts index 4a94efb11..6c62b7b10 100644 --- a/packages/react/src/api/updateOrganization.ts +++ b/packages/react/src/api/updateOrganization.ts @@ -18,6 +18,7 @@ import { HttpInstance, + HttpResponse, AsgardeoSPAClient, HttpRequestConfig, updateOrganization as baseUpdateOrganization, @@ -89,18 +90,18 @@ const updateOrganization = async ({ ...requestConfig }: UpdateOrganizationConfig): Promise => { const defaultFetcher = async (url: string, config: RequestInit): Promise => { - const response = await httpClient({ - url, - method: config.method || 'PATCH', - headers: config.headers as Record, + const response: HttpResponse = await httpClient({ data: config.body ? JSON.parse(config.body as string) : undefined, + headers: config.headers as Record, + method: config.method || 'PATCH', + url, } as HttpRequestConfig); return { + json: () => Promise.resolve(response.data), ok: response.status >= 200 && response.status < 300, status: response.status, statusText: response.statusText || '', - json: () => Promise.resolve(response.data), text: () => Promise.resolve(typeof response.data === 'string' ? response.data : JSON.stringify(response.data)), } as Response; }; diff --git a/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx b/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx index d4c2adf5c..30da101cf 100644 --- a/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx +++ b/packages/react/src/components/actions/SignUpButton/SignUpButton.tsx @@ -16,8 +16,7 @@ * under the License. */ -import {AsgardeoRuntimeError} from '@asgardeo/browser'; -import {navigate} from '@asgardeo/browser'; +import {AsgardeoRuntimeError, navigate} from '@asgardeo/browser'; import {forwardRef, ForwardRefExoticComponent, MouseEvent, ReactElement, Ref, RefAttributes, useState} from 'react'; import BaseSignUpButton, {BaseSignUpButtonProps} from './BaseSignUpButton'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; diff --git a/packages/react/src/components/adapters/CheckboxInput.tsx b/packages/react/src/components/adapters/CheckboxInput.tsx index 317f6a56f..8e428ca0a 100644 --- a/packages/react/src/components/adapters/CheckboxInput.tsx +++ b/packages/react/src/components/adapters/CheckboxInput.tsx @@ -18,8 +18,8 @@ import {FieldType} from '@asgardeo/browser'; import {FC} from 'react'; -import {createField} from '../factories/FieldFactory'; import {AdapterProps} from '../../models/adapters'; +import {createField} from '../factories/FieldFactory'; /** * Checkbox input component for sign-up forms. @@ -31,22 +31,22 @@ const CheckboxInput: FC = ({ formErrors, onInputChange, inputClassName, -}) => { +}: AdapterProps) => { const config: Record = component.config || {}; const fieldName: string = (config['identifier'] as string) || (config['name'] as string) || component.id; const value: string | boolean = (formValues[fieldName] as string) || false; const error: string | undefined = touchedFields[fieldName] ? formErrors[fieldName] : undefined; return createField({ - type: FieldType.Checkbox, - name: fieldName, + className: inputClassName, + error, label: (config['label'] as string) || '', + name: fieldName, + onChange: (newValue: string) => onInputChange(fieldName, newValue), placeholder: (config['placeholder'] as string) || '', required: (config['required'] as boolean) || false, + type: FieldType.Checkbox, value: value as string, - error, - onChange: (newValue: string) => onInputChange(fieldName, newValue), - className: inputClassName, }); }; diff --git a/packages/react/src/components/adapters/DateInput.tsx b/packages/react/src/components/adapters/DateInput.tsx index 87bece07b..93b241761 100644 --- a/packages/react/src/components/adapters/DateInput.tsx +++ b/packages/react/src/components/adapters/DateInput.tsx @@ -18,8 +18,8 @@ import {FieldType} from '@asgardeo/browser'; import {FC} from 'react'; -import {createField} from '../factories/FieldFactory'; import {AdapterProps} from '../../models/adapters'; +import {createField} from '../factories/FieldFactory'; /** * Date input component for sign-up forms. @@ -31,22 +31,22 @@ const DateInput: FC = ({ formErrors, onInputChange, inputClassName, -}) => { +}: AdapterProps) => { const config: Record = component.config || {}; const fieldName: string = (config['identifier'] as string) || (config['name'] as string) || component.id; const value: string = formValues[fieldName] || ''; const error: string | undefined = touchedFields[fieldName] ? formErrors[fieldName] : undefined; return createField({ - type: FieldType.Date, - name: fieldName, + className: inputClassName, + error, label: (config['label'] as string) || '', + name: fieldName, + onChange: (newValue: string) => onInputChange(fieldName, newValue), placeholder: (config['placeholder'] as string) || '', required: (config['required'] as boolean) || false, + type: FieldType.Date, value, - error, - onChange: (newValue: string) => onInputChange(fieldName, newValue), - className: inputClassName, }); }; diff --git a/packages/react/src/components/adapters/DividerComponent.tsx b/packages/react/src/components/adapters/DividerComponent.tsx index b771077d4..a9806cd8c 100644 --- a/packages/react/src/components/adapters/DividerComponent.tsx +++ b/packages/react/src/components/adapters/DividerComponent.tsx @@ -17,18 +17,18 @@ */ import {FC} from 'react'; -import Divider from '../primitives/Divider/Divider'; import useTheme from '../../contexts/Theme/useTheme'; import {AdapterProps} from '../../models/adapters'; +import Divider from '../primitives/Divider/Divider'; /** * Divider component for sign-up forms. */ -const DividerComponent: FC = ({component}) => { +const DividerComponent: FC = ({component}: AdapterProps) => { const {theme} = useTheme(); const config: Record = component.config || {}; - const text = (config['text'] as string) || ''; - const variant = (component.variant?.toLowerCase() as string) || 'horizontal'; + const text: string = (config['text'] as string) || ''; + const variant: string = (component.variant?.toLowerCase() as string) || 'horizontal'; return ( = ({ formErrors, onInputChange, inputClassName, -}) => { +}: AdapterProps) => { const config: Record = component.config || {}; const fieldName: string = (config['identifier'] as string) || (config['name'] as string) || component.id; const value: string = formValues[fieldName] || ''; const error: string | undefined = touchedFields[fieldName] ? formErrors[fieldName] : undefined; return createField({ - type: FieldType.Email, - name: fieldName, + className: inputClassName, + error, label: (config['label'] as string) || 'Email', + name: fieldName, + onChange: (newValue: string) => onInputChange(fieldName, newValue), placeholder: (config['placeholder'] as string) || 'Enter your email', required: (config['required'] as boolean) || false, + type: FieldType.Email, value, - error, - onChange: (newValue: string) => onInputChange(fieldName, newValue), - className: inputClassName, }); }; diff --git a/packages/react/src/components/adapters/FacebookButton.tsx b/packages/react/src/components/adapters/FacebookButton.tsx index 27d910393..cafce1665 100644 --- a/packages/react/src/components/adapters/FacebookButton.tsx +++ b/packages/react/src/components/adapters/FacebookButton.tsx @@ -16,10 +16,10 @@ * under the License. */ -import {FC, HTMLAttributes} from 'react'; import {WithPreferences} from '@asgardeo/browser'; -import Button from '../primitives/Button/Button'; +import {FC, HTMLAttributes} from 'react'; import useTranslation from '../../hooks/useTranslation'; +import Button from '../primitives/Button/Button'; export interface FacebookButtonProps extends WithPreferences { /** @@ -37,7 +37,7 @@ const FacebookButton: FC preferences, children, ...rest -}) => { +}: FacebookButtonProps & HTMLAttributes) => { const {t} = useTranslation(preferences?.i18n); return ( diff --git a/packages/react/src/components/adapters/FormContainer.tsx b/packages/react/src/components/adapters/FormContainer.tsx index 05b44f9d2..3b1030a54 100644 --- a/packages/react/src/components/adapters/FormContainer.tsx +++ b/packages/react/src/components/adapters/FormContainer.tsx @@ -16,24 +16,25 @@ * under the License. */ -import {FC} from 'react'; -import {createSignUpComponent} from '../presentation/auth/SignUp/v1/SignUpOptionFactory'; +import {FC, FormEvent} from 'react'; import {AdapterProps} from '../../models/adapters'; +// eslint-disable-next-line import/no-cycle +import {createSignUpComponent} from '../presentation/auth/SignUp/v1/SignUpOptionFactory'; /** * Form container component that renders child components. */ -const FormContainer: FC = props => { +const FormContainer: FC = (props: AdapterProps) => { const {component} = props; // If the form has child components, render them wrapped in a form element if (component.components && component.components.length > 0) { - const handleFormSubmit = (e: React.FormEvent): void => { + const handleFormSubmit: (e: FormEvent) => void = (e: FormEvent): void => { e.preventDefault(); // Find submit button in child components and trigger its submission - const submitButton = component.components?.find( - child => + const submitButton: any = component.components?.find( + (child: any) => child.type === 'BUTTON' && (child.variant === 'PRIMARY' || child.variant === 'SECONDARY' || child.config?.['type'] === 'submit'), ); @@ -45,7 +46,7 @@ const FormContainer: FC = props => { return (
- {component.components.map((childComponent, index) => + {component.components.map((childComponent: any) => createSignUpComponent({ ...props, component: childComponent, diff --git a/packages/react/src/components/adapters/GitHubButton.tsx b/packages/react/src/components/adapters/GitHubButton.tsx index 38d96c368..04fce162d 100644 --- a/packages/react/src/components/adapters/GitHubButton.tsx +++ b/packages/react/src/components/adapters/GitHubButton.tsx @@ -16,10 +16,10 @@ * under the License. */ -import {FC, HTMLAttributes} from 'react'; import {WithPreferences} from '@asgardeo/browser'; -import Button from '../primitives/Button/Button'; +import {FC, HTMLAttributes} from 'react'; import useTranslation from '../../hooks/useTranslation'; +import Button from '../primitives/Button/Button'; export interface GithubButtonProps extends WithPreferences { /** @@ -37,7 +37,7 @@ const GitHubButton: FC> = preferences, children, ...rest -}) => { +}: GithubButtonProps & HTMLAttributes) => { const {t} = useTranslation(preferences?.i18n); return ( diff --git a/packages/react/src/components/adapters/GoogleButton.tsx b/packages/react/src/components/adapters/GoogleButton.tsx index dc314387f..6ef7abc52 100644 --- a/packages/react/src/components/adapters/GoogleButton.tsx +++ b/packages/react/src/components/adapters/GoogleButton.tsx @@ -16,8 +16,8 @@ * under the License. */ -import {FC, HTMLAttributes} from 'react'; import {WithPreferences} from '@asgardeo/browser'; +import {FC, HTMLAttributes} from 'react'; import useTranslation from '../../hooks/useTranslation'; import Button from '../primitives/Button/Button'; @@ -37,7 +37,7 @@ const GoogleButton: FC> = preferences, children, ...rest -}) => { +}: GoogleButtonProps & HTMLAttributes) => { const {t} = useTranslation(preferences?.i18n); return ( diff --git a/packages/react/src/components/adapters/ImageComponent.tsx b/packages/react/src/components/adapters/ImageComponent.tsx index 1470fe217..38516fc98 100644 --- a/packages/react/src/components/adapters/ImageComponent.tsx +++ b/packages/react/src/components/adapters/ImageComponent.tsx @@ -16,26 +16,26 @@ * under the License. */ -import {FC} from 'react'; +import {CSSProperties, FC, SyntheticEvent} from 'react'; import useTheme from '../../contexts/Theme/useTheme'; import {AdapterProps} from '../../models/adapters'; /** * Image component for sign-up forms. */ -const ImageComponent: FC = ({component}) => { +const ImageComponent: FC = ({component}: AdapterProps) => { const {theme} = useTheme(); const config: Record = component.config || {}; - const src = (config['src'] as string) || ''; - const alt = (config['alt'] as string) || (config['label'] as string) || 'Image'; - const variant = (component.variant?.toLowerCase() as string) || 'image_block'; + const src: string = (config['src'] as string) || ''; + const alt: string = (config['alt'] as string) || (config['label'] as string) || 'Image'; + const variant: string = (component.variant?.toLowerCase() as string) || 'image_block'; - const imageStyle: React.CSSProperties = { - maxWidth: '100%', - height: 'auto', + const imageStyle: CSSProperties = { + borderRadius: theme.vars.borderRadius.small, display: 'block', + height: 'auto', margin: variant === 'image_block' ? '1rem auto' : '0', - borderRadius: theme.vars.borderRadius.small, + maxWidth: '100%', }; if (!src) { @@ -48,7 +48,7 @@ const ImageComponent: FC = ({component}) => { src={src} alt={alt} style={imageStyle} - onError={e => { + onError={(e: SyntheticEvent): void => { // Hide broken images e.currentTarget.style.display = 'none'; }} diff --git a/packages/react/src/components/adapters/LinkedInButton.tsx b/packages/react/src/components/adapters/LinkedInButton.tsx index 417f2c384..b1c9001a1 100644 --- a/packages/react/src/components/adapters/LinkedInButton.tsx +++ b/packages/react/src/components/adapters/LinkedInButton.tsx @@ -16,10 +16,10 @@ * under the License. */ -import {FC, HTMLAttributes} from 'react'; -import Button from '../primitives/Button/Button'; import {WithPreferences} from '@asgardeo/browser'; +import {FC, HTMLAttributes} from 'react'; import useTranslation from '../../hooks/useTranslation'; +import Button from '../primitives/Button/Button'; export interface LinkedInButtonProps extends WithPreferences { /** @@ -37,7 +37,7 @@ const LinkedInButton: FC preferences, children, ...rest -}) => { +}: LinkedInButtonProps & HTMLAttributes) => { const {t} = useTranslation(preferences?.i18n); return ( diff --git a/packages/react/src/components/adapters/MicrosoftButton.tsx b/packages/react/src/components/adapters/MicrosoftButton.tsx index 0c1494161..6f410c8bb 100644 --- a/packages/react/src/components/adapters/MicrosoftButton.tsx +++ b/packages/react/src/components/adapters/MicrosoftButton.tsx @@ -16,10 +16,10 @@ * under the License. */ -import {FC, HTMLAttributes} from 'react'; -import Button from '../primitives/Button/Button'; import {WithPreferences} from '@asgardeo/browser'; +import {FC, HTMLAttributes} from 'react'; import useTranslation from '../../hooks/useTranslation'; +import Button from '../primitives/Button/Button'; export interface MicrosoftButtonProps extends WithPreferences { /** @@ -37,7 +37,7 @@ const MicrosoftButton: FC { +}: MicrosoftButtonProps & HTMLAttributes) => { const {t} = useTranslation(preferences?.i18n); return ( diff --git a/packages/react/src/components/adapters/NumberInput.tsx b/packages/react/src/components/adapters/NumberInput.tsx index 85e26c306..019c9ed6c 100644 --- a/packages/react/src/components/adapters/NumberInput.tsx +++ b/packages/react/src/components/adapters/NumberInput.tsx @@ -18,8 +18,8 @@ import {FieldType} from '@asgardeo/browser'; import {FC} from 'react'; -import {createField} from '../factories/FieldFactory'; import {AdapterProps} from '../../models/adapters'; +import {createField} from '../factories/FieldFactory'; /** * Number input component for sign-up forms. @@ -31,22 +31,22 @@ const NumberInput: FC = ({ formErrors, onInputChange, inputClassName, -}) => { +}: AdapterProps) => { const config: Record = component.config || {}; const fieldName: string = (config['identifier'] as string) || (config['name'] as string) || component.id; const value: string = formValues[fieldName] || ''; const error: string | undefined = touchedFields[fieldName] ? formErrors[fieldName] : undefined; return createField({ - type: FieldType.Number, - name: fieldName, + className: inputClassName, + error, label: (config['label'] as string) || '', + name: fieldName, + onChange: (newValue: string) => onInputChange(fieldName, newValue), placeholder: (config['placeholder'] as string) || '', required: (config['required'] as boolean) || false, + type: FieldType.Number, value, - error, - onChange: (newValue: string) => onInputChange(fieldName, newValue), - className: inputClassName, }); }; diff --git a/packages/react/src/components/adapters/PasswordInput.tsx b/packages/react/src/components/adapters/PasswordInput.tsx index 8f61df0e4..f6a539087 100644 --- a/packages/react/src/components/adapters/PasswordInput.tsx +++ b/packages/react/src/components/adapters/PasswordInput.tsx @@ -18,8 +18,8 @@ import {FieldType} from '@asgardeo/browser'; import {FC} from 'react'; -import {createField} from '../factories/FieldFactory'; import {AdapterProps} from '../../models/adapters'; +import {createField} from '../factories/FieldFactory'; /** * Password input component for sign-up forms. @@ -31,7 +31,7 @@ const PasswordInput: FC = ({ formErrors, onInputChange, inputClassName, -}) => { +}: AdapterProps) => { const config: Record = component.config || {}; const fieldName: string = (config['identifier'] as string) || (config['name'] as string) || component.id; const value: string = formValues[fieldName] || ''; @@ -50,46 +50,44 @@ const PasswordInput: FC = ({ validations.forEach((validation: any) => { if (validation.name === 'LengthValidator') { - const minLength = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; - const maxLength = validation.conditions?.find((c: any) => c.key === 'max.length')?.value; + const minLength: string | undefined = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; + const maxLength: string | undefined = validation.conditions?.find((c: any) => c.key === 'max.length')?.value; if (minLength || maxLength) { validationHints.push(`Length: ${minLength || '0'}-${maxLength || '∞'} characters`); } } else if (validation.name === 'UpperCaseValidator') { - const minLength = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; + const minLength: string | undefined = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; if (minLength && parseInt(minLength, 10) > 0) { validationHints.push('Must contain uppercase letter(s)'); } } else if (validation.name === 'LowerCaseValidator') { - const minLength = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; + const minLength: string | undefined = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; if (minLength && parseInt(minLength, 10) > 0) { validationHints.push('Must contain lowercase letter(s)'); } } else if (validation.name === 'NumeralValidator') { - const minLength = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; + const minLength: string | undefined = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; if (minLength && parseInt(minLength, 10) > 0) { validationHints.push('Must contain number(s)'); } } else if (validation.name === 'SpecialCharacterValidator') { - const minLength = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; + const minLength: string | undefined = validation.conditions?.find((c: any) => c.key === 'min.length')?.value; if (minLength && parseInt(minLength, 10) > 0) { validationHints.push('Must contain special character(s)'); } } }); - const hint = validationHints.length > 0 ? validationHints.join(', ') : config['hint'] || ''; - return createField({ - type: FieldType.Password, - name: fieldName, + className: inputClassName, + error, label: (config['label'] as string) || 'Password', + name: fieldName, + onChange: (newValue: string) => onInputChange(fieldName, newValue), placeholder: (config['placeholder'] as string) || 'Enter your password', required: (config['required'] as boolean) || false, + type: FieldType.Password, value, - error, - onChange: (newValue: string) => onInputChange(fieldName, newValue), - className: inputClassName, }); }; diff --git a/packages/react/src/components/adapters/SelectInput.tsx b/packages/react/src/components/adapters/SelectInput.tsx index 1784c3614..12578df02 100644 --- a/packages/react/src/components/adapters/SelectInput.tsx +++ b/packages/react/src/components/adapters/SelectInput.tsx @@ -18,9 +18,9 @@ import {FieldType} from '@asgardeo/browser'; import {FC} from 'react'; +import {AdapterProps} from '../../models/adapters'; import {createField} from '../factories/FieldFactory'; import {SelectOption} from '../primitives/Select/Select'; -import {AdapterProps} from '../../models/adapters'; /** * Select input component for sign-up forms. @@ -32,7 +32,7 @@ const SelectInput: FC = ({ formErrors, onInputChange, inputClassName, -}) => { +}: AdapterProps) => { const config: Record = component.config || {}; const fieldName: string = (config['identifier'] as string) || (config['name'] as string) || component.id; const value: string = formValues[fieldName] || ''; @@ -46,16 +46,16 @@ const SelectInput: FC = ({ })); return createField({ - type: FieldType.Select, - name: fieldName, + className: inputClassName, + error, label: (config['label'] as string) || '', + name: fieldName, + onChange: (newValue: string): void => onInputChange(fieldName, newValue), + options, placeholder: (config['placeholder'] as string) || '', required: (config['required'] as boolean) || false, + type: FieldType.Select, value, - error, - options, - onChange: (newValue: string): void => onInputChange(fieldName, newValue), - className: inputClassName, }); }; diff --git a/packages/react/src/components/adapters/SignInWithEthereumButton.tsx b/packages/react/src/components/adapters/SignInWithEthereumButton.tsx index 75582ad6c..f43224db3 100644 --- a/packages/react/src/components/adapters/SignInWithEthereumButton.tsx +++ b/packages/react/src/components/adapters/SignInWithEthereumButton.tsx @@ -16,10 +16,10 @@ * under the License. */ +import {WithPreferences} from '@asgardeo/browser'; import {FC, HTMLAttributes} from 'react'; -import Button from '../primitives/Button/Button'; import useTranslation from '../../hooks/useTranslation'; -import {WithPreferences} from '@asgardeo/browser'; +import Button from '../primitives/Button/Button'; export interface SignInWithEthereumButtonProps extends WithPreferences { /** @@ -37,7 +37,7 @@ const SignInWithEthereumButton: FC { +}: SignInWithEthereumButtonProps & HTMLAttributes) => { const {t} = useTranslation(preferences?.i18n); return ( diff --git a/packages/react/src/components/adapters/SmsOtpButton.tsx b/packages/react/src/components/adapters/SmsOtpButton.tsx index 9166340c1..dfe6feb3c 100644 --- a/packages/react/src/components/adapters/SmsOtpButton.tsx +++ b/packages/react/src/components/adapters/SmsOtpButton.tsx @@ -16,10 +16,10 @@ * under the License. */ -import {FC, HTMLAttributes} from 'react'; -import Button from '../primitives/Button/Button'; import {WithPreferences} from '@asgardeo/browser'; +import {FC, HTMLAttributes} from 'react'; import useTranslation from '../../hooks/useTranslation'; +import Button from '../primitives/Button/Button'; export interface SmsOtpButtonProps extends WithPreferences { /** @@ -37,7 +37,7 @@ const SmsOtpButton: FC> = preferences, children, ...rest -}) => { +}: SmsOtpButtonProps & HTMLAttributes) => { const {t} = useTranslation(preferences?.i18n); return ( diff --git a/packages/react/src/components/adapters/SocialButton.tsx b/packages/react/src/components/adapters/SocialButton.tsx index 031c95055..3aa3810ed 100644 --- a/packages/react/src/components/adapters/SocialButton.tsx +++ b/packages/react/src/components/adapters/SocialButton.tsx @@ -16,14 +16,20 @@ * under the License. */ -import {FC} from 'react'; -import Button from '../primitives/Button/Button'; +import {FC, ReactElement} from 'react'; import {AdapterProps} from '../../models/adapters'; +import Button from '../primitives/Button/Button'; /** * Social button component for sign-up forms. */ -const SocialButton: FC = ({component, isLoading, buttonClassName, size = 'medium', onSubmit}) => { +const SocialButton: FC = ({ + component, + isLoading, + buttonClassName, + size = 'medium', + onSubmit, +}: AdapterProps): ReactElement => { const config: Record = component.config || {}; const buttonText: string = (config['text'] as string) || (config['label'] as string) || 'Continue with Social'; diff --git a/packages/react/src/components/adapters/SubmitButton.tsx b/packages/react/src/components/adapters/SubmitButton.tsx index f2fddbe32..16a7a3456 100644 --- a/packages/react/src/components/adapters/SubmitButton.tsx +++ b/packages/react/src/components/adapters/SubmitButton.tsx @@ -17,9 +17,9 @@ */ import {FC} from 'react'; +import {AdapterProps} from '../../models/adapters'; import Button from '../primitives/Button/Button'; import Spinner from '../primitives/Spinner/Spinner'; -import {AdapterProps} from '../../models/adapters'; /** * Button component for sign-up forms that handles all button variants. @@ -31,31 +31,31 @@ const ButtonComponent: FC = ({ buttonClassName, onSubmit, size = 'medium', -}) => { +}: AdapterProps) => { const config: Record = component.config || {}; const buttonText: string = (config['text'] as string) || (config['label'] as string) || 'Continue'; const buttonType: string = (config['type'] as string) || 'submit'; const componentVariant: string = component.variant?.toUpperCase() || 'PRIMARY'; // Map component variants to Button primitive props - const getButtonProps = () => { + const getButtonProps = (): {color: 'primary' | 'secondary'; variant: 'solid' | 'text' | 'outline'} => { switch (componentVariant) { case 'PRIMARY': - return {variant: 'solid' as const, color: 'primary' as const}; + return {color: 'primary' as const, variant: 'solid' as const}; case 'SECONDARY': - return {variant: 'solid' as const, color: 'secondary' as const}; + return {color: 'secondary' as const, variant: 'solid' as const}; case 'TEXT': - return {variant: 'text' as const, color: 'primary' as const}; + return {color: 'primary' as const, variant: 'text' as const}; case 'SOCIAL': - return {variant: 'outline' as const, color: 'primary' as const}; + return {color: 'primary' as const, variant: 'outline' as const}; default: - return {variant: 'solid' as const, color: 'primary' as const}; + return {color: 'primary' as const, variant: 'solid' as const}; } }; const {variant, color} = getButtonProps(); - const handleClick = () => { + const handleClick = (): void => { if (onSubmit && buttonType !== 'submit') { onSubmit(component); } diff --git a/packages/react/src/components/adapters/TelephoneInput.tsx b/packages/react/src/components/adapters/TelephoneInput.tsx index 308c2f000..9f2f2a419 100644 --- a/packages/react/src/components/adapters/TelephoneInput.tsx +++ b/packages/react/src/components/adapters/TelephoneInput.tsx @@ -16,9 +16,9 @@ * under the License. */ -import {FC} from 'react'; -import TextField from '../primitives/TextField/TextField'; +import {ChangeEvent, FC} from 'react'; import {AdapterProps} from '../../models/adapters'; +import TextField from '../primitives/TextField/TextField'; /** * Telephone input component for sign-up forms. @@ -30,7 +30,7 @@ const TelephoneInput: FC = ({ formErrors, onInputChange, inputClassName, -}) => { +}: AdapterProps) => { const config: Record = component.config || {}; const fieldName: string = (config['identifier'] as string) || (config['name'] as string) || component.id; const value: string = formValues[fieldName] || ''; @@ -46,7 +46,7 @@ const TelephoneInput: FC = ({ required={(config['required'] as boolean) || false} value={value} error={error} - onChange={e => onInputChange(fieldName, e.target.value)} + onChange={(e: ChangeEvent): void => onInputChange(fieldName, e.target.value)} className={inputClassName} helperText={(config['hint'] as string) || ''} /> diff --git a/packages/react/src/components/adapters/TextInput.tsx b/packages/react/src/components/adapters/TextInput.tsx index b0ac77628..72c834909 100644 --- a/packages/react/src/components/adapters/TextInput.tsx +++ b/packages/react/src/components/adapters/TextInput.tsx @@ -18,8 +18,8 @@ import {FieldType} from '@asgardeo/browser'; import {FC} from 'react'; -import {createField} from '../factories/FieldFactory'; import {AdapterProps} from '../../models/adapters'; +import {createField} from '../factories/FieldFactory'; /** * Text input component for sign-up forms. @@ -31,22 +31,22 @@ const TextInput: FC = ({ formErrors, onInputChange, inputClassName, -}) => { +}: AdapterProps) => { const config: Record = component.config || {}; const fieldName: string = (config['identifier'] as string) || (config['name'] as string) || component.id; - const value = formValues[fieldName] || ''; - const error = touchedFields[fieldName] ? formErrors[fieldName] : undefined; + const value: string = formValues[fieldName] || ''; + const error: string | undefined = touchedFields[fieldName] ? formErrors[fieldName] : undefined; return createField({ - type: FieldType.Text, - name: fieldName, + className: inputClassName, + error, label: (config['label'] as string) || '', + name: fieldName, + onChange: (newValue: string) => onInputChange(fieldName, newValue), placeholder: (config['placeholder'] as string) || '', required: (config['required'] as boolean) || false, + type: FieldType.Text, value, - error, - onChange: (newValue: string) => onInputChange(fieldName, newValue), - className: inputClassName, }); }; diff --git a/packages/react/src/components/adapters/Typography.tsx b/packages/react/src/components/adapters/Typography.tsx index da285f7a4..c23856011 100644 --- a/packages/react/src/components/adapters/Typography.tsx +++ b/packages/react/src/components/adapters/Typography.tsx @@ -17,14 +17,14 @@ */ import {FC} from 'react'; -import Typography from '../primitives/Typography/Typography'; import useTheme from '../../contexts/Theme/useTheme'; import {AdapterProps} from '../../models/adapters'; +import Typography from '../primitives/Typography/Typography'; /** * Typography component for sign-up forms (titles, descriptions, etc.). */ -const TypographyComponent: FC = ({component}) => { +const TypographyComponent: FC = ({component}: AdapterProps) => { const {theme} = useTheme(); const config: Record = component.config || {}; const text: string = (config['text'] as string) || (config['content'] as string) || ''; diff --git a/packages/react/src/components/factories/FieldFactory.tsx b/packages/react/src/components/factories/FieldFactory.tsx index fd7b4f693..cd12896d6 100644 --- a/packages/react/src/components/factories/FieldFactory.tsx +++ b/packages/react/src/components/factories/FieldFactory.tsx @@ -16,73 +16,71 @@ * under the License. */ -import {FC, ReactElement} from 'react'; -import TextField from '../primitives/TextField/TextField'; -import Select from '../primitives/Select/Select'; -import {SelectOption} from '../primitives/Select/Select'; +import {FieldType} from '@asgardeo/browser'; +import {ChangeEvent, FC, ReactElement} from 'react'; +import Checkbox from '../primitives/Checkbox/Checkbox'; +import DatePicker from '../primitives/DatePicker/DatePicker'; import OtpField from '../primitives/OtpField/OtpField'; import PasswordField from '../primitives/PasswordField/PasswordField'; -import DatePicker from '../primitives/DatePicker/DatePicker'; -import Checkbox from '../primitives/Checkbox/Checkbox'; -import {FieldType} from '@asgardeo/browser'; +import Select, {SelectOption} from '../primitives/Select/Select'; +import TextField from '../primitives/TextField/TextField'; /** * Interface for field configuration. */ export interface FieldConfig { /** - * The name of the field. - */ - name: string; - /** - * The field type. + * Additional CSS class name. */ - type: FieldType - ; + className?: string; /** - * Display name for the field. + * Whether the field is disabled. */ - label: string; + disabled?: boolean; /** - * Whether the field is required. + * Error message to display. */ - required: boolean; + error?: string; /** - * Current value of the field. + * Display name for the field. */ - value: string; + label: string; /** - * Callback function when the field value changes. + * The name of the field. */ - onChange: (value: string) => void; + name: string; /** * Callback function when the field loses focus. */ onBlur?: () => void; /** - * Whether the field is disabled. + * Callback function when the field value changes. */ - disabled?: boolean; + onChange: (value: string) => void; /** - * Error message to display. + * Additional options for multi-valued fields. */ - error?: string; + options?: SelectOption[]; /** - * Additional CSS class name. + * Placeholder text for the field. */ - className?: string; + placeholder?: string; /** - * Additional options for multi-valued fields. + * Whether the field is required. */ - options?: SelectOption[]; + required: boolean; /** * Whether the field has been touched/interacted with by the user. */ touched?: boolean; /** - * Placeholder text for the field. + * The field type. */ - placeholder?: string; + type: FieldType; + /** + * Current value of the field. + */ + value: string; } /** @@ -103,12 +101,15 @@ export const validateFieldValue = ( } switch (type) { - case FieldType.Number: - const numValue = parseInt(value, 10); - if (isNaN(numValue)) { + case FieldType.Number: { + const numValue: number = parseInt(value, 10); + if (Number.isNaN(numValue)) { return 'Please enter a valid number'; } break; + } + default: + break; } return null; @@ -150,53 +151,78 @@ export const createField = (config: FieldConfig): ReactElement => { placeholder, } = config; - const validationError = error || validateFieldValue(value, type, required, touched); + const validationError: string | null = error || validateFieldValue(value, type, required, touched); - const commonProps = { - name, - label, - required, + const commonProps: Record = { + className, + 'data-testid': `asgardeo-signin-${name}`, disabled, error: validationError, - className, - value, - placeholder, + label, + name, onBlur, - 'data-testid': `asgardeo-signin-${name}`, + placeholder, + required, + value, }; switch (type) { case FieldType.Password: return ; case FieldType.Text: - return onChange(e.target.value)} autoComplete="off" />; + return ( + ): void => onChange(e.target.value)} + autoComplete="off" + /> + ); case FieldType.Email: - return onChange(e.target.value)} autoComplete="email" />; + return ( + ): void => onChange(e.target.value)} + autoComplete="email" + /> + ); case FieldType.Date: - return onChange(e.target.value)} />; - case FieldType.Checkbox: - const isChecked = value === 'true' || (value as any) === true; - return onChange(e.target.checked.toString())} />; + return ( + ): void => onChange(e.target.value)} /> + ); + case FieldType.Checkbox: { + const isChecked: boolean = value === 'true' || (value as any) === true; + return ( + ): void => onChange(e.target.checked.toString())} + /> + ); + } case FieldType.Otp: - return onChange(e.target.value)} />; + return ( + ): void => onChange(e.target.value)} /> + ); case FieldType.Number: return ( onChange(e.target.value)} + onChange={(e: ChangeEvent): void => onChange(e.target.value)} helperText="Enter a numeric value" /> ); - case FieldType.Select: - const fieldOptions = options.length > 0 ? options : []; + case FieldType.Select: { + const fieldOptions: SelectOption[] = options.length > 0 ? options : []; if (fieldOptions.length > 0) { return (