diff --git a/docs/en/framework/ui/react/authorization.md b/docs/en/framework/ui/react/authorization.md new file mode 100644 index 0000000000..4f6d673d87 --- /dev/null +++ b/docs/en/framework/ui/react/authorization.md @@ -0,0 +1,183 @@ +```json +//[doc-seo] +{ + "Description": "Learn how authentication and authorization are configured in ABP React UI applications." +} +``` + +# Authorization in React UI + +OAuth is preconfigured in ABP React UI templates. When you create a React solution with ABP Studio v3.0+ or `abp new --modern --ui-framework react`, the template includes OpenID Connect settings, an OpenIddict client, route guards, and authentication hooks. + +The React app authenticates against the ABP Auth Server using the **Authorization Code flow with PKCE**, which is the recommended flow for browser-based applications. + +## Packages + +The template uses these packages for authentication: + +| Package | Purpose | +| --- | --- | +| `@volo/abp-oidc-auth` | Framework-agnostic OIDC client helpers for ABP/OpenIddict backends. | +| `@volo/abp-react-oidc-auth` | React adapter for the ABP OIDC client. | +| `oidc-client-ts` | Underlying OIDC protocol implementation. | + +The package list also includes `@volo/abp-app-config` and `@volo/abp-react-app-config`, which are used to fetch application configuration and permissions after authentication. + +## OAuth Configuration + +The OIDC settings are resolved from runtime configuration first and fall back to `src/env.ts`. + +```ts +export function getOAuthConfig(): { + issuer: string + redirectUri: string + clientId: string + scope: string + responseType: 'code' +} { + return { + issuer: loadedConfig?.oAuthConfig?.issuer ?? env.oauth.issuer, + redirectUri: loadedConfig?.oAuthConfig?.redirectUri ?? env.oauth.redirectUri, + clientId: loadedConfig?.oAuthConfig?.clientId ?? env.oauth.clientId, + scope: loadedConfig?.oAuthConfig?.scope ?? env.oauth.scope, + responseType: 'code', + } +} +``` + +The important configuration values are: + +- `oAuthConfig.issuer`: Auth Server / OpenIddict authority URL. +- `oAuthConfig.redirectUri`: URL where the Auth Server redirects after login. +- `oAuthConfig.clientId`: OpenIddict client ID, normally `_App`. +- `oAuthConfig.scope`: Scopes requested by the React app. + +See [Environment Variables](./environment-variables.md) for the full runtime configuration model. + +## Initializing Authentication + +The app loads runtime configuration before initializing OIDC: + +```tsx +async function bootstrap() { + await loadRuntimeConfig() + initUserManager() + createRoot(document.getElementById('root')!).render( + + + + ) +} +``` + +`initUserManager()` creates the ABP React OIDC client: + +```ts +client = createAbpReactOidcAuth({ + authority: config.issuer, + clientId: config.clientId, + redirectUri: config.redirectUri, + postLogoutRedirectUri: config.redirectUri, + scope: config.scope, + responseType: config.responseType, + automaticSilentRenew: true, + userStoreType: 'localStorage', + userStorePrefix: `oidc.${config.clientId}`, + silentRedirectUri: `${window.location.origin}/silent-renew.html`, +}) +``` + +The template stores the OIDC user in local storage and enables silent renewal with `public/silent-renew.html`. + +## Auth Provider and Hook + +`AuthProvider` wraps the app and handles the OIDC callback: + +```tsx +export function AuthProvider({ children }: { children: ReactNode }) { + const authClient = getAuthClient() + + useEffect(() => { + const params = new URLSearchParams(window.location.search) + if (!params.has('code') || !params.has('state')) return + void authClient.handleSigninCallback().then(() => + window.history.replaceState({}, document.title, window.location.pathname) + ) + }, []) + + return {children} +} +``` + +Use `useAuth()` in components: + +```tsx +import { useAuth } from '@/lib/auth/AuthContext' + +export function LoginButton() { + const { isAuthenticated, isLoading, login, logout, user } = useAuth() + + if (isLoading) return null + + return isAuthenticated ? ( + + ) : ( + + ) +} +``` + +## Route Protection + +The React template uses TanStack Router. Protected routes use `beforeLoad` guards. + +```ts +const identityLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/identity', + component: IdentityLayout, + beforeLoad: authGuard, +}) +``` + +`authGuard` checks the current OIDC user and redirects unauthenticated users to the Auth Server: + +```ts +export async function authGuard({ location }: GuardContext) { + const user = await userManager.getUser() + if (!user || user.expired) { + await userManager.signinRedirect({ + state: { returnUrl: location.href }, + }) + throw new Error('Redirecting to login') + } +} +``` + +Routes that also require a permission use `createPermissionGuard`: + +```ts +const usersRoute = createRoute({ + getParentRoute: () => identityLayoutRoute, + path: 'users', + component: UsersPage, + beforeLoad: createPermissionGuard('AbpIdentity.Users'), +}) +``` + +Permission checks are explained in [Permission Management](./permission-management.md). + +## OpenIddict Clients + +The generated OpenIddict clients depend on the template: + +- Layered and single-layer modern templates use the main React client, normally `_App`. +- Microservice modern templates also include an Admin Console client, normally `_AdminConsole`, because the Admin Console is a separate React app. + +If you change URLs after generation, update both the runtime configuration and the corresponding OpenIddict client redirect URLs. + +## See Also + +- [Environment Variables](./environment-variables.md) +- [Permission Management](./permission-management.md) +- [Authorization](../../../framework/fundamentals/authorization/index.md) diff --git a/docs/en/framework/ui/react/http-requests.md b/docs/en/framework/ui/react/http-requests.md new file mode 100644 index 0000000000..2c01884635 --- /dev/null +++ b/docs/en/framework/ui/react/http-requests.md @@ -0,0 +1,213 @@ +```json +//[doc-seo] +{ + "Description": "Learn how HTTP requests are made in ABP React UI applications with Axios, runtime configuration, and ABP interceptors." +} +``` + +# HTTP Requests + +ABP React UI templates use [Axios](https://axios-http.com/) for HTTP requests. The generated app contains a shared Axios instance with ABP-specific request and response interceptors, plus typed API modules for backend endpoints. + +The shared client is defined in `src/lib/api/axios.ts` and exported as `api`. + +## Base URL + +The Axios base URL is resolved at request time from runtime configuration: + +```ts +export function getApiBaseUrl(): string { + const apiUrl = getApiUrl() + if (apiUrl.startsWith('http://') || apiUrl.startsWith('https://')) { + return apiUrl.replace(/\/$/, '') + '/api' + } + if (import.meta.env.DEV) { + return '/api' + } + return apiUrl.replace(/\/$/, '') + '/api' +} +``` + +The API URL comes from: + +1. `dynamic-env.json` -> `apis.default.url` +2. `VITE_API_URL` +3. `src/env.ts` generated fallback + +In microservice solutions, `apis.default.url` normally points to the Web Gateway. In layered and single-layer solutions, it normally points to the HTTP API host. + +## Shared Axios Instance + +The template creates one shared instance: + +```ts +export const api = axios.create({ + baseURL: '', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/json', + }, +}) +``` + +Use this instance for application API modules instead of creating new Axios clients. It centralizes ABP headers, authentication, tenant handling, language handling, and redirects. + +## Request Interceptor + +Before each request, the template: + +- Sets `baseURL` from runtime configuration. +- Adds `Authorization: Bearer ` from the OIDC user. +- Adds `__tenant` when the user has selected a tenant. +- Adds `Accept-Language` from i18next. +- Keeps default AJAX headers such as `X-Requested-With`. + +```ts +api.interceptors.request.use(async (config) => { + config.baseURL = getApiBaseUrl() + + const user = await userManager.getUser() + if (user?.access_token) { + config.headers.Authorization = `Bearer ${user.access_token}` + } + + const tenantId = sessionStorage.getItem('abp_tenant_id') + if (tenantId && !config.headers.__tenant) { + config.headers.__tenant = tenantId + } + + if (i18n?.language) { + config.headers['Accept-Language'] = + config.headers['Accept-Language'] ?? i18n.language + } + + return config +}) +``` + +## Response Interceptor + +The response interceptor handles common authorization failures: + +- `401 Unauthorized`: redirects to login unless `skipAuthRedirect` is set. +- `403 Forbidden`: redirects to `/403` unless `skip403Redirect` is set. +- Other errors are rejected so the caller can handle them. + +```ts +api.interceptors.response.use( + (response) => response, + async (error) => { + const status = error.response?.status + + if (status === 401 && !error.config?.skipAuthRedirect) { + await userManager.signinRedirect() + return Promise.reject(new Error('Unauthorized - redirecting to login')) + } + + if (status === 403 && !error.config?.skip403Redirect) { + window.location.href = '/403' + return Promise.reject(new Error('Forbidden')) + } + + return Promise.reject(error) + } +) +``` + +Use `skipAuthRedirect` or `skip403Redirect` for calls where the component should handle the error itself. + +## Typed API Modules + +The template organizes backend calls under `src/lib/api/`. For example, the Books sample defines DTOs and functions in `books.ts`: + +```ts +import { api } from './axios' + +export interface PagedResultDto { + items: T[] + totalCount: number +} + +export interface BookDto { + id: string + name?: string + price: number +} + +export async function getBooks(): Promise> { + const { data } = await api.get>('/app/book', { + params: { + maxResultCount: 10, + skipCount: 0, + }, + }) + return data +} +``` + +Notice that the API module calls `/app/book`, not `/api/app/book`. The shared Axios base URL already includes the `/api` prefix when needed. + +## Using Requests from Components + +The template uses TanStack Query for server state: + +```tsx +const { data, isLoading } = useQuery({ + queryKey: ['books', skipCount], + queryFn: () => + getBooks({ + maxResultCount: 10, + skipCount, + sorting: 'creationTime desc', + }), +}) +``` + +Mutations use `useMutation` and invalidate related queries after success: + +```tsx +const createMutation = useMutation({ + mutationFn: createBook, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['books'] }) + toast.success(t('AbpUi::SavedSuccessfully')) + }, +}) +``` + +## Adding a New API Module + +Create a file under `src/lib/api/`: + +```ts +import { api } from './axios' + +export interface ProductDto { + id: string + name: string +} + +export async function getProducts(): Promise { + const { data } = await api.get('/app/product') + return data +} +``` + +Then consume it from a component with TanStack Query: + +```tsx +const productsQuery = useQuery({ + queryKey: ['products'], + queryFn: getProducts, +}) +``` + +## Development Proxy + +In development, Vite proxies `/api`, `/connect`, and `/getEnvConfig`. This lets the React app use same-origin paths while calls are forwarded to the backend, Auth Server, or gateway configured by `VITE_API_URL` and `VITE_AUTH_URL`. + +## See Also + +- [Environment Variables](./environment-variables.md) +- [Authorization](./authorization.md) +- [Permission Management](./permission-management.md) diff --git a/docs/en/framework/ui/react/localization.md b/docs/en/framework/ui/react/localization.md new file mode 100644 index 0000000000..64298278f0 --- /dev/null +++ b/docs/en/framework/ui/react/localization.md @@ -0,0 +1,158 @@ +```json +//[doc-seo] +{ + "Description": "Learn how localization works in ABP React UI applications with i18next and ABP application configuration." +} +``` + +# Localization + +ABP React UI templates use [i18next](https://www.i18next.com/) with [react-i18next](https://react.i18next.com/). The generated app includes local JSON resources and integrates with ABP application configuration through the `@volo/abp-app-config` packages. + +## Localization Files + +The main React app stores client-side translations under `src/locales/`. + +```text +src/ +├── locales/ +│ └── en.json +└── lib/ + └── i18n/ + └── i18n.ts +``` + +The default `i18n.ts` imports the English resource and registers it: + +```ts +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import en from '@/locales/en.json' + +i18n.use(initReactI18next).init({ + resources: { + en: { translation: en }, + }, + lng: 'en', + fallbackLng: 'en', + keySeparator: false, + nsSeparator: false, + interpolation: { + escapeValue: false, + }, +}) +``` + +`keySeparator` and `nsSeparator` are disabled so ABP-style keys such as `AbpIdentity::Users` and `Menu:Home` can be used directly. + +## Using Localized Text + +Use `useTranslation()` from `react-i18next` in components: + +```tsx +import { useTranslation } from 'react-i18next' + +export function BooksTitle() { + const { t } = useTranslation() + + return

{t('Menu:Books')}

+} +``` + +ABP localization keys commonly use the `ResourceName::Key` format: + +```tsx +{t('AbpIdentity::Users')} +{t('AbpAccount::Login')} +{t('AbpUi::SavedSuccessfully')} +``` + +Application-specific menu keys may use names like `Menu:Home` or `Menu:Books`. + +## Adding a Translation + +Add the key to `src/locales/en.json`: + +```json +{ + "Menu:Reports": "Reports", + "Reports": "Reports" +} +``` + +Then use it from a component: + +```tsx +const { t } = useTranslation() + +return

{t('Reports')}

+``` + +## Adding a Language + +Create a new JSON file, for example `src/locales/tr.json`: + +```json +{ + "Menu:Reports": "Raporlar", + "Reports": "Raporlar" +} +``` + +Register it in `src/lib/i18n/i18n.ts`: + +```ts +import en from '@/locales/en.json' +import tr from '@/locales/tr.json' + +i18n.use(initReactI18next).init({ + resources: { + en: { translation: en }, + tr: { translation: tr }, + }, + lng: 'en', + fallbackLng: 'en', +}) +``` + +If you add a language selector, call `i18n.changeLanguage('tr')` when the user chooses Turkish. + +## Server-Side ABP Localization + +ABP's backend localization system is still the source of truth for server-defined resources, validation messages, exception messages, and module texts. The React app uses ABP application configuration through `@volo/abp-app-config` / `@volo/abp-react-app-config` for auth and configuration data, and these packages can include localization resources when configured to do so. + +The main template currently creates the app configuration client with: + +```ts +export const appConfig = createAbpReactAppConfig({ + baseUrl: () => getApiUrl(), + includeLocalizationResources: false, +}) +``` + +Because `includeLocalizationResources` is disabled in the main React template, UI text is normally loaded from `src/locales/*.json`. If you enable server-provided localization resources, make sure your UI initialization merges them into i18next before rendering localized components. + +## Request Culture + +The shared Axios client sends the active i18next language with each request: + +```ts +if (i18n?.language) { + config.headers['Accept-Language'] = + config.headers['Accept-Language'] ?? i18n.language +} +``` + +This lets backend responses, validation messages, and exception messages use the selected culture when the server supports it. + +## Admin Console Localization + +The Admin Console has its own React app and localization setup. In layered and single-layer templates, it is served from the `Volo.Abp.AdminConsole` package. In microservice templates, it is generated as `apps/react-admin-console/`. + +The Admin Console host can expose available languages through `AdminConsole:LocalizationLanguages`, and `/admin-console/api/config` returns the normalized language list. + +## See Also + +- [React UI](./index.md) +- [HTTP Requests](./http-requests.md) +- [Localization](../../../framework/fundamentals/localization.md) diff --git a/docs/en/framework/ui/react/permission-management.md b/docs/en/framework/ui/react/permission-management.md new file mode 100644 index 0000000000..fc009ce5a2 --- /dev/null +++ b/docs/en/framework/ui/react/permission-management.md @@ -0,0 +1,171 @@ +```json +//[doc-seo] +{ + "Description": "Learn how permissions are fetched, stored, checked, and applied in ABP React UI applications." +} +``` + +# Permission Management + +ABP permissions are defined on the server side and are exposed to the React app through ABP application configuration. The React template uses those permissions to protect routes, hide sidebar items, and conditionally render UI actions. + +For the server-side permission system, see [Authorization](../../../framework/fundamentals/authorization/index.md). + +## Packages + +The React template uses: + +| Package | Purpose | +| --- | --- | +| `@volo/abp-app-config` | Framework-agnostic ABP application configuration client. | +| `@volo/abp-react-app-config` | React hooks and adapters for application configuration. | + +The template creates a shared app configuration client in `src/lib/auth/permissions.ts`: + +```ts +export const appConfig = createAbpReactAppConfig({ + baseUrl: () => getApiUrl(), + includeLocalizationResources: false, +}) +``` + +## Fetching Permissions + +After the user logs in, `AuthProvider` fetches application configuration with the current access token: + +```ts +const user = await authClient.getUserManager().getUser() +if (user && !user.expired) { + await fetchAppConfig(user.access_token ?? null) +} +``` + +`fetchAppConfig` also sends the current tenant ID when one is selected: + +```ts +export async function fetchAppConfig(token: string | null): Promise { + const headers: Record = {} + const tenantId = sessionStorage.getItem('abp_tenant_id') + if (tenantId) headers.__tenant = tenantId + await appConfig.fetchConfig(token, { headers }) +} +``` + +The response includes the current user's granted policies. These are stored by the app configuration client and exposed to React components. + +## Checking Permissions in Components + +Use `usePermissions()` from `src/lib/auth/permissions.ts`: + +```tsx +import { usePermissions } from '@/lib/auth/permissions' + +export function BookActions() { + const { isGranted } = usePermissions() + + return ( + <> + {isGranted('MyProjectName.Books.Edit') && } + {isGranted('MyProjectName.Books.Delete') && } + + ) +} +``` + +The Books page uses this pattern for edit and delete actions: + +```ts +const { isGranted } = usePermissions() +const canEdit = isGranted('MyProjectName.Books.Edit') +const canDelete = isGranted('MyProjectName.Books.Delete') +``` + +## Route Guards + +Routes can require a permission by using `createPermissionGuard`: + +```ts +const booksRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/books', + component: BooksPage, + beforeLoad: createPermissionGuard('MyProjectName.Books'), +}) +``` + +`createPermissionGuard` runs the authentication guard first, fetches app configuration if needed, and redirects to `/403` when the required policy is not granted. + +```ts +export function createPermissionGuard(requiredPolicy: string) { + return async (context: GuardContext) => { + await authGuard(context) + + if (!appConfig.getSnapshot()?.initialized) { + const user = await userManager.getUser() + await fetchAppConfig(user?.access_token ?? null) + } + + if (!isPolicyGranted(requiredPolicy)) throw redirect({ to: '/403' }) + } +} +``` + +## Sidebar Visibility + +The sidebar reads `routeConfig` and hides items that require missing permissions: + +```ts +export const routeConfig: RouteConfigItem[] = [ + { + path: '/identity/users', + nameKey: 'AbpIdentity::Users', + requiredPolicy: 'AbpIdentity.Users', + }, +] +``` + +The sidebar checks each item: + +```ts +if (item.requiresAuth && !isAuthenticated) return false +if (!item.requiredPolicy) return true +if (!isAuthenticated) return false +return isGranted(item.requiredPolicy) +``` + +Use `requiresAuth` for menu items that only require login. Use `requiredPolicy` when the item should only be visible to users with a specific permission. + +## Compound Policies + +The template's `isPolicyGranted` helper supports simple compound expressions: + +- `PermissionA || PermissionB` +- `PermissionA && PermissionB` + +This is useful for menu entries that should be visible when the user has one of several related module permissions. + +## Where Permissions Are Applied + +The generated React app uses permissions in these places: + +- **Users page**: the `/identity/users` route and sidebar entry require `AbpIdentity.Users`. The page links to the Admin Console for full user and role management. +- **Books page**: the route requires `MyProjectName.Books`; edit and delete actions check `MyProjectName.Books.Edit` and `MyProjectName.Books.Delete`. +- **Admin Console link**: the sidebar entry uses `requiresAuth` because the Admin Console performs its own module and route permission checks. + +The Admin Console applies module-specific permissions for pages such as: + +- Identity users and roles: `AbpIdentity.*`. +- OpenIddict applications and scopes: `OpenIddictPro.Application` and `OpenIddictPro.Scope`. +- Audit Logging UI: `AuditLogging.AuditLogs`. +- Text Template Management: `TextTemplateManagement.*`. +- AI Management: `AIManagement.*`. + +## Multi-Tenancy + +When a tenant is selected, the template stores the tenant ID in `sessionStorage` as `abp_tenant_id`. Permission and API requests send it with the `__tenant` header. This ensures the backend returns permissions and data for the selected tenant context. + +## See Also + +- [Authorization](./authorization.md) +- [HTTP Requests](./http-requests.md) +- [Authorization](../../../framework/fundamentals/authorization/index.md)