Browse Source

Merge pull request #23416 from abpframework/feature/ssr-support

Hybrid Rendering & Application Builder
pull/23758/head
oykuermann 1 year ago
committed by GitHub
parent
commit
54e11ae828
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      npm/ng-packs/.prettierignore
  2. 29
      npm/ng-packs/apps/dev-app/project.json
  3. 5
      npm/ng-packs/apps/dev-app/src/app/app.component.ts
  4. 29
      npm/ng-packs/apps/dev-app/src/app/app.config.server.ts
  5. 17
      npm/ng-packs/apps/dev-app/src/app/app.config.ts
  6. 8
      npm/ng-packs/apps/dev-app/src/app/app.routes.server.ts
  7. 2
      npm/ng-packs/apps/dev-app/src/app/route.provider.ts
  8. 1
      npm/ng-packs/apps/dev-app/src/environments/environment.prod.ts
  9. 1
      npm/ng-packs/apps/dev-app/src/environments/environment.ts
  10. 7
      npm/ng-packs/apps/dev-app/src/main.server.ts
  11. 187
      npm/ng-packs/apps/dev-app/src/server.ts
  12. 9
      npm/ng-packs/apps/dev-app/tsconfig.app.json
  13. 10
      npm/ng-packs/apps/dev-app/tsconfig.json
  14. 11
      npm/ng-packs/nx.json
  15. 35
      npm/ng-packs/package.json
  16. 2
      npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts
  17. 10
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html
  18. 5
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts
  19. 113
      npm/ng-packs/packages/core/locale/src/utils/register-locale.ts
  20. 2
      npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts
  21. 2
      npm/ng-packs/packages/core/src/lib/abstracts/auth.service.ts
  22. 3
      npm/ng-packs/packages/core/src/lib/core.module.ts
  23. 9
      npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts
  24. 1
      npm/ng-packs/packages/core/src/lib/interceptors/index.ts
  25. 41
      npm/ng-packs/packages/core/src/lib/interceptors/timezone.interceptor.ts
  26. 42
      npm/ng-packs/packages/core/src/lib/interceptors/transfer-state.interceptor.ts
  27. 2
      npm/ng-packs/packages/core/src/lib/models/environment.ts
  28. 8
      npm/ng-packs/packages/core/src/lib/providers/cookie-language.provider.ts
  29. 12
      npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts
  30. 99
      npm/ng-packs/packages/core/src/lib/services/cookie-storage.service.ts
  31. 28
      npm/ng-packs/packages/core/src/lib/services/dom-strategy.service.ts
  32. 2
      npm/ng-packs/packages/core/src/lib/services/index.ts
  33. 32
      npm/ng-packs/packages/core/src/lib/services/local-storage.service.ts
  34. 16
      npm/ng-packs/packages/core/src/lib/services/session-state.service.ts
  35. 1
      npm/ng-packs/packages/core/src/lib/services/window.service.ts
  36. 6
      npm/ng-packs/packages/core/src/lib/strategies/content.strategy.ts
  37. 17
      npm/ng-packs/packages/core/src/lib/strategies/dom.strategy.ts
  38. 1
      npm/ng-packs/packages/core/src/lib/tokens/index.ts
  39. 19
      npm/ng-packs/packages/core/src/lib/tokens/ssr-state.token.ts
  40. 1
      npm/ng-packs/packages/core/src/lib/utils/index.ts
  41. 3
      npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts
  42. 29
      npm/ng-packs/packages/core/src/lib/utils/multi-tenancy-utils.ts
  43. 44
      npm/ng-packs/packages/core/src/lib/utils/server-cookie-utils.ts
  44. 5
      npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts
  45. 15
      npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.ts
  46. 6
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts
  47. 40
      npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts
  48. 15
      npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts
  49. 36
      npm/ng-packs/packages/oauth/src/lib/services/browser-token-storage.service.ts
  50. 4
      npm/ng-packs/packages/oauth/src/lib/services/index.ts
  51. 15
      npm/ng-packs/packages/oauth/src/lib/services/oauth.service.ts
  52. 81
      npm/ng-packs/packages/oauth/src/lib/services/remember-me.service.ts
  53. 32
      npm/ng-packs/packages/oauth/src/lib/services/server-token-storage.service.ts
  54. 124
      npm/ng-packs/packages/oauth/src/lib/strategies/auth-code-flow-strategy.ts
  55. 12
      npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts
  56. 7
      npm/ng-packs/packages/oauth/src/lib/strategies/auth-password-flow-strategy.ts
  57. 2
      npm/ng-packs/packages/oauth/src/lib/utils/check-access-token.ts
  58. 5
      npm/ng-packs/packages/oauth/src/lib/utils/clear-o-auth-storage.ts
  59. 1
      npm/ng-packs/packages/oauth/src/lib/utils/index.ts
  60. 39
      npm/ng-packs/packages/oauth/src/lib/utils/storage.factory.ts
  61. 8
      npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts
  62. 2
      npm/ng-packs/packages/setting-management/config/src/lib/components/email-setting-group/email-setting-group.component.ts
  63. 6
      npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.ts
  64. 6
      npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts
  65. 5
      npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts
  66. 28
      npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.html
  67. 6
      npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts
  68. 6
      npm/ng-packs/packages/theme-basic/src/lib/handlers/lazy-style.handler.ts
  69. 6
      npm/ng-packs/packages/theme-basic/src/lib/services/layout.service.ts
  70. 5
      npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts
  71. 49
      npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts
  72. 5
      npm/ng-packs/packages/theme-shared/src/lib/handlers/document-dir.handler.ts
  73. 2
      npm/ng-packs/packages/theme-shared/src/lib/providers/tenant-not-found.provider.ts
  74. 4
      npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts
  75. 8
      templates/app-nolayers/angular/angular.json
  76. 2
      templates/app-nolayers/angular/karma.conf.js
  77. 1
      templates/app-nolayers/angular/package.json
  78. 1
      templates/app-nolayers/angular/tsconfig.json
  79. 8
      templates/app/angular/angular.json
  80. 2
      templates/app/angular/karma.conf.js
  81. 3
      templates/app/angular/package.json
  82. 1
      templates/app/angular/tsconfig.json
  83. 12
      templates/module/angular/angular.json
  84. 1
      templates/module/angular/package.json
  85. 2
      templates/module/angular/projects/dev-app/karma.conf.js
  86. 2
      templates/module/angular/projects/my-project-name/karma.conf.js
  87. 1
      templates/module/angular/tsconfig.json

3
npm/ng-packs/.prettierignore

@ -9,4 +9,5 @@
/tools
/source-code-requirements
/.nx/cache
/.nx/workspace-data
/.nx/workspace-data
.angular

29
npm/ng-packs/apps/dev-app/project.json

@ -6,13 +6,12 @@
"prefix": "app",
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",
"executor": "@angular/build:application",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/dev-app",
"index": "apps/dev-app/src/index.html",
"browser": "apps/dev-app/src/main.ts",
"polyfills": ["apps/dev-app/src/polyfills.ts"],
"polyfills": ["apps/dev-app/src/polyfills.ts", "zone.js"],
"tsConfig": "apps/dev-app/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"allowedCommonJsDependencies": ["chart.js", "js-sha256"],
@ -115,7 +114,12 @@
},
"apps/dev-app/src/styles.scss"
],
"scripts": []
"scripts": [],
"server": "apps/dev-app/src/main.server.ts",
"ssr": {
"entry": "apps/dev-app/src/server.ts"
},
"outputMode": "server"
},
"configurations": {
"production": {
@ -149,7 +153,8 @@
"defaultConfiguration": "production"
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
"continuous": true,
"executor": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "dev-app:build:production"
@ -171,10 +176,22 @@
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/apps/dev-app"],
"outputs": [
"{workspaceRoot}/coverage/apps/dev-app"
],
"options": {
"jestConfig": "apps/dev-app/jest.config.ts"
}
},
"serve-static": {
"continuous": true,
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "dev-app:build",
"port": 4200,
"staticFilePath": "dist/apps/dev-app/browser",
"spa": true
}
}
}
}

5
npm/ng-packs/apps/dev-app/src/app/app.component.ts

@ -1,5 +1,5 @@
import { Component } from '@angular/core';
import { InternetConnectionStatusComponent, LoaderBarComponent } from '@abp/ng.theme.shared';
import { LoaderBarComponent } from '@abp/ng.theme.shared';
import { DynamicLayoutComponent } from '@abp/ng.core';
@Component({
@ -7,8 +7,7 @@ import { DynamicLayoutComponent } from '@abp/ng.core';
template: `
<abp-loader-bar></abp-loader-bar>
<abp-dynamic-layout></abp-dynamic-layout>
<abp-internet-status></abp-internet-status>
`,
imports: [LoaderBarComponent, DynamicLayoutComponent, InternetConnectionStatusComponent],
imports: [LoaderBarComponent, DynamicLayoutComponent],
})
export class AppComponent {}

29
npm/ng-packs/apps/dev-app/src/app/app.config.server.ts

@ -0,0 +1,29 @@
import {
mergeApplicationConfig,
ApplicationConfig,
provideAppInitializer,
inject,
PLATFORM_ID,
TransferState
} from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { appServerRoutes } from './app.routes.server';
import { SSR_FLAG } from '@abp/ng.core';
const serverConfig: ApplicationConfig = {
providers: [
provideAppInitializer(() => {
const platformId = inject(PLATFORM_ID);
const transferState = inject<TransferState>(TransferState);
if (isPlatformServer(platformId)) {
transferState.set(SSR_FLAG, true);
}
}),
provideServerRendering(withRoutes(appServerRoutes)),
],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);

17
npm/ng-packs/apps/dev-app/src/app/app.config.ts

@ -1,11 +1,17 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import {
provideClientHydration,
withEventReplay,
withHttpTransferCacheOptions,
withIncrementalHydration,
} from '@angular/platform-browser';
import { appRoutes } from './app.routes';
import { APP_ROUTE_PROVIDER } from './route.provider';
import { provideAbpCore, withOptions } from '@abp/ng.core';
import { environment } from '../environments/environment';
import { registerLocale } from '@abp/ng.core/locale';
import { registerLocaleForEsBuild } from '@abp/ng.core/locale';
import { provideAbpOAuth } from '@abp/ng.oauth';
import { provideAbpThemeShared } from '@abp/ng.theme.shared';
import { provideSettingManagementConfig } from '@abp/ng.setting-management/config';
@ -18,12 +24,11 @@ import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(appRoutes),
APP_ROUTE_PROVIDER,
provideAbpCore(
withOptions({
environment,
registerLocaleFn: registerLocale(),
registerLocaleFn: registerLocaleForEsBuild(),
sendNullsAsQueryParam: false,
skipGetAppConfiguration: false,
}),
@ -38,5 +43,11 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }),
provideThemeBasicConfig(),
provideAnimations(),
provideRouter(appRoutes),
provideClientHydration(
withEventReplay(),
withHttpTransferCacheOptions({}),
withIncrementalHydration(),
),
],
};

8
npm/ng-packs/apps/dev-app/src/app/app.routes.server.ts

@ -0,0 +1,8 @@
import { RenderMode, ServerRoute } from '@angular/ssr';
export const appServerRoutes: ServerRoute[] = [
{
path: '**',
renderMode: RenderMode.Server,
}
];

2
npm/ng-packs/apps/dev-app/src/app/route.provider.ts

@ -16,6 +16,6 @@ function configureRoutes() {
iconClass: 'fas fa-home',
order: 1,
layout: eLayoutType.application,
},
}
]);
}

1
npm/ng-packs/apps/dev-app/src/environments/environment.prod.ts

@ -16,6 +16,7 @@ export const environment = {
clientId: 'MyProjectName_App',
responseType: 'code',
scope: 'offline_access MyProjectName',
ssrAuthenticationUrl: '/authorize'
},
apis: {
default: {

1
npm/ng-packs/apps/dev-app/src/environments/environment.ts

@ -16,6 +16,7 @@ export const environment = {
scope: 'offline_access MyProjectName',
responseType: 'code',
redirectUri: baseUrl,
ssrAuthorizationUrl: '/authorize'
},
apis: {
default: {

7
npm/ng-packs/apps/dev-app/src/main.server.ts

@ -0,0 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';
const bootstrap = () => bootstrapApplication(AppComponent, config);
export default bootstrap;

187
npm/ng-packs/apps/dev-app/src/server.ts

@ -0,0 +1,187 @@
import {
AngularNodeAppEngine,
createNodeRequestHandler,
isMainModule,
writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {environment} from './environments/environment';
import * as oidc from 'openid-client';
import { ServerCookieParser } from '@abp/ng.core';
if (environment.production === false) {
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
}
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const app = express();
const angularApp = new AngularNodeAppEngine();
const ISSUER = new URL(environment.oAuthConfig.issuer);
const CLIENT_ID = environment.oAuthConfig.clientId;
const REDIRECT_URI = environment.oAuthConfig.redirectUri;
const SCOPE = environment.oAuthConfig.scope;
const config = await oidc.discovery(ISSUER, CLIENT_ID, /* client_secret */ undefined);
const secureCookie = { httpOnly: true, sameSite: 'lax' as const, secure: environment.production, path: '/' };
const tokenCookie = { ...secureCookie, httpOnly: false };
app.use(ServerCookieParser.middleware());
const sessions = new Map<string, { pkce?: string; state?: string; refresh?: string; at?: string, returnUrl?: string }>();
app.get('/authorize', async (_req, res) => {
const code_verifier = oidc.randomPKCECodeVerifier();
const code_challenge = await oidc.calculatePKCECodeChallenge(code_verifier);
const state = oidc.randomState();
if (_req.query.returnUrl) {
const returnUrl = String(_req.query.returnUrl || null);
res.cookie('returnUrl', returnUrl, { ...secureCookie, maxAge: 5 * 60 * 1000 });
}
const sid = crypto.randomUUID();
sessions.set(sid, { pkce: code_verifier, state });
res.cookie('sid', sid, secureCookie);
const url = oidc.buildAuthorizationUrl(config, {
redirect_uri: REDIRECT_URI,
scope: SCOPE,
code_challenge,
code_challenge_method: 'S256',
state,
});
res.redirect(url.toString());
});
app.get('/logout', async (req, res) => {
try {
const sid = req.cookies.sid;
if (sid && sessions.has(sid)) {
sessions.delete(sid);
}
res.clearCookie('sid', secureCookie);
res.clearCookie('access_token', tokenCookie);
res.clearCookie('refresh_token', secureCookie);
res.clearCookie('expires_at', tokenCookie);
res.clearCookie('returnUrl', secureCookie);
const endSessionEndpoint = config.serverMetadata().end_session_endpoint;
if (endSessionEndpoint) {
const logoutUrl = new URL(endSessionEndpoint);
logoutUrl.searchParams.set('post_logout_redirect_uri', REDIRECT_URI);
logoutUrl.searchParams.set('client_id', CLIENT_ID);
return res.redirect(logoutUrl.toString());
}
res.redirect('/');
} catch (error) {
console.error('Logout error:', error);
res.status(500).send('Logout error');
}
});
app.get('/', async (req, res, next) => {
try {
const { code, state } = req.query as any;
if (!code || !state) return next();
const sid = req.cookies.sid;
const sess = sid && sessions.get(sid);
if (!sess || state !== sess.state) return res.status(400).send('invalid state');
const tokenEndpoint = config.serverMetadata().token_endpoint!;
const body = new URLSearchParams({
grant_type: 'authorization_code',
code: String(code),
redirect_uri: environment.oAuthConfig.redirectUri,
code_verifier: sess.pkce!,
client_id: CLIENT_ID
});
const resp = await fetch(tokenEndpoint, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body,
});
if (!resp.ok) {
const errTxt = await resp.text();
console.error('token error:', resp.status, errTxt);
return res.status(500).send('token error');
}
const tokens = await resp.json();
const expiresInSec =
Number(tokens.expires_in ?? tokens.expiresIn ?? 3600);
const skewSec = 60;
const accessExpiresAt = new Date(
Date.now() + Math.max(0, expiresInSec - skewSec) * 1000
);
sessions.set(sid, { ...sess, at: tokens.access_token, refresh: tokens.refresh_token });
res.cookie('access_token', tokens.access_token, {...tokenCookie, maxAge: accessExpiresAt.getTime()});
res.cookie('refresh_token', tokens.refresh_token, secureCookie);
res.cookie('expires_at', String(accessExpiresAt.getTime()), tokenCookie);
const returnUrl = req.cookies?.returnUrl ?? '/';
res.clearCookie('returnUrl', secureCookie);
return res.redirect(returnUrl);
} catch (e) {
console.error('OIDC error:', e);
return res.status(500).send('oidc error');
}
});
/**
* Serve static files from /browser
*/
app.use(
express.static(browserDistFolder, {
maxAge: '1y',
index: false,
redirect: false,
}),
);
/**
* Handle all other requests by rendering the Angular application.
*/
app.use((req, res, next) => {
angularApp
.handle(req)
.then(response => {
if (response) {
res.cookie('ssr-init', 'true', {...secureCookie, httpOnly: false});
return writeResponseToNodeResponse(response, res);
} else {
return next()
}
})
.catch(next);
});
/**
* Start the server if this module is the main entry point.
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
*/
if (isMainModule(import.meta.url)) {
const port = process.env['PORT'] || 4200;
app.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
/**
* Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
*/
export const reqHandler = createNodeRequestHandler(app);

9
npm/ng-packs/apps/dev-app/tsconfig.app.json

@ -2,11 +2,8 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [],
"target": "ES2022",
"useDefineForClassFields": false
"types": ["node"]
},
"files": ["src/main.ts", "src/polyfills.ts"],
"include": ["src/**/*.d.ts"],
"exclude": ["jest.config.ts"]
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/test-setup.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
}

10
npm/ng-packs/apps/dev-app/tsconfig.json

@ -14,15 +14,19 @@
}
],
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"strict": false,
"noImplicitReturns": false,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"target": "es2022",
"moduleResolution": "bundler",
"module": "preserve",
"allowSyntheticDefaultImports": true,
"target": "es2020"
"strictPropertyInitialization": false,
"useDefineForClassFields": false
},
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictInjectionParameters": false,
"strictInputAccessModifiers": false,
"strictTemplates": false
}

11
npm/ng-packs/nx.json

@ -9,10 +9,10 @@
"defaultProject": "dev-app",
"generators": {
"@nx/angular:application": {
"style": "scss",
"e2eTestRunner": "cypress",
"linter": "eslint",
"unitTestRunner": "jest",
"e2eTestRunner": "cypress"
"style": "scss",
"unitTestRunner": "jest"
},
"@nx/angular:library": {
"linter": "eslint",
@ -113,6 +113,11 @@
"@nx/eslint:lint": {
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
"cache": true
},
"@angular/build:application": {
"cache": true,
"dependsOn": ["^build"],
"inputs": ["production", "^production"]
}
},
"namedInputs": {

35
npm/ng-packs/package.json

@ -38,7 +38,11 @@
"lerna": "lerna",
"migrate-nx": "yarn nx migrate --run-migrations",
"copy-to:app": "cd scripts && yarn && yarn copy-to-templates -t app",
"update-version": "nx generate @abp/nx.generators:update-version"
"update-version": "nx generate @abp/nx.generators:update-version",
"dev:ssr": "nx serve-ssr dev-app",
"serve:ssr": "node dist/dev-app/server/main.js",
"build:ssr": "ng build && ng run dev-app:server",
"prerender": "ng run dev-app:prerender"
},
"private": true,
"devDependencies": {
@ -52,7 +56,8 @@
"@angular-eslint/eslint-plugin-template": "~20.0.0",
"@angular-eslint/template-parser": "~20.0.0",
"@angular/animations": "~20.0.0",
"@angular/cli": "~20.0.1",
"@angular/build": "~20.0.0",
"@angular/cli": "~20.0.0",
"@angular/common": "~20.0.0",
"@angular/compiler": "~20.0.0",
"@angular/compiler-cli": "~20.0.0",
@ -62,7 +67,9 @@
"@angular/localize": "~20.0.0",
"@angular/platform-browser": "~20.0.0",
"@angular/platform-browser-dynamic": "~20.0.0",
"@angular/platform-server": "~20.0.0",
"@angular/router": "~20.0.0",
"@angular/ssr": "~20.0.0",
"@fortawesome/fontawesome-free": "^6.0.0",
"@ng-bootstrap/ng-bootstrap": "~19.0.0",
"@ngneat/spectator": "~19.6.2",
@ -75,6 +82,7 @@
"@nx/jest": "~21.2.0",
"@nx/js": "~21.2.0",
"@nx/plugin": "~21.2.0",
"@nx/web": "~21.2.0",
"@nx/workspace": "~21.2.0",
"@popperjs/core": "~2.11.0",
"@schematics/angular": "~20.0.0",
@ -83,21 +91,24 @@
"@swc/core": "~1.5.0",
"@swc/helpers": "~0.5.0",
"@swimlane/ngx-datatable": "~21.1.0",
"@types/express": "~5.0.0",
"@types/jest": "29.5.14",
"@types/node": "^20.0.0",
"@types/node": "~20.11.0",
"@typescript-eslint/eslint-plugin": "7.16.0",
"@typescript-eslint/parser": "7.16.0",
"@typescript-eslint/utils": "^7.16.0",
"angular-oauth2-oidc": "^20.0.0",
"angular-oauth2-oidc": "~20.0.0",
"autoprefixer": "^10.4.21",
"bootstrap": "^5.0.0",
"bootstrap-icons": "^1.0.0",
"chart.js": "^4.0.0",
"bootstrap": "~5.0.0",
"bootstrap-icons": "~1.13.0",
"browser-sync": "~3.0.0",
"chart.js": "~4.0.0",
"cypress": "^7.0.0",
"dotenv": "10.0.0",
"eslint": "^8.0.0",
"eslint": "~8.0.0",
"eslint-config-prettier": "10.0.0",
"eslint-plugin-cypress": "^2.10.3",
"eslint-plugin-cypress": "^3.5.0",
"express": "~5.1.0",
"got": "^11.0.0",
"jest": "^29.0.0",
"jest-canvas-mock": "^2.0.0",
@ -123,7 +134,7 @@
"ts-jest": "29.1.0",
"ts-node": "10.9.1",
"ts-toolbelt": "^9.0.0",
"tslib": "^2.0.0",
"tslib": "^2.3.0",
"tslint": "~6.1.0",
"typescript": "~5.8.0",
"zone.js": "~0.15.0"
@ -133,5 +144,7 @@
"npx prettier --write --config .prettierrc "
]
},
"dependencies": {}
"dependencies": {
"openid-client": "^6.6.4"
}
}

2
npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts

@ -1,7 +1,7 @@
import { ProfileService } from '@abp/ng.account.core/proxy';
import { fadeIn, LoadingDirective } from '@abp/ng.theme.shared';
import { transition, trigger, useAnimation } from '@angular/animations';
import { Component, OnInit, inject } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';
import { eAccountComponents } from '../../enums/components';
import { ManageProfileStateService } from '../../services/manage-profile.state.service';
import { NgClass, AsyncPipe } from '@angular/common';

10
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html

@ -1,4 +1,5 @@
<ngx-datatable
@if (isBrowser) {
<ngx-datatable
default
[rows]="data"
[count]="recordsTotal"
@ -15,7 +16,7 @@
>
@if(selectable) {
<ngx-datatable-column [width]="50" [sortable]="false" [canAutoResize]="false" [draggable]="false" [resizeable]="false">
<ng-template ngx-datatable-header-template let-value="value" let-allRowsSelected="allRowsSelected"
let-selectFn="selectFn">
@if (_selectionType !== 'single') {
@ -25,7 +26,7 @@
</div>
}
</ng-template>
<ng-template ngx-datatable-cell-template let-value="value" let-row="row" let-isSelected="isSelected"
let-onCheckboxChangeFn="onCheckboxChangeFn">
@if(_selectionType === 'single') {
@ -39,7 +40,7 @@
</div>
}
</ng-template>
</ngx-datatable-column>
}
@if (actionsTemplate || (actionList.length && hasAtLeastOnePermittedAction)) {
@ -136,3 +137,4 @@
</ngx-datatable-column>
}
</ngx-datatable>
}

5
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts

@ -12,12 +12,13 @@ import {
OnChanges,
OnDestroy,
Output,
PLATFORM_ID,
signal,
SimpleChanges,
TemplateRef,
TrackByFunction,
} from '@angular/core';
import { AsyncPipe, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
import { Observable, filter, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs';
@ -84,6 +85,8 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
protected readonly timeZoneService = inject(TimezoneService);
protected readonly entityPropTypeClasses = inject(ENTITY_PROP_TYPE_CLASSES);
protected readonly permissionService = inject(PermissionService);
private platformId = inject(PLATFORM_ID);
protected isBrowser = isPlatformBrowser(this.platformId);
protected _actionsText!: string;
@Input()

113
npm/ng-packs/packages/core/locale/src/utils/register-locale.ts

@ -1,5 +1,6 @@
import { differentLocales } from '@abp/ng.core';
import { isDevMode } from '@angular/core';
import { inject, isDevMode, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
export interface LocaleErrorHandlerData {
resolve: any;
@ -15,72 +16,70 @@ export interface RegisterLocaleData {
errorHandlerFn?: (data: LocaleErrorHandlerData) => any;
}
function loadLocale(locale: string) {
// hard coded list works with esbuild. Source https://github.com/angular/angular-cli/issues/26904#issuecomment-1903596563
const list = {
'ar': () => import('@angular/common/locales/ar'),
'cs': () => import('@angular/common/locales/cs'),
'en': () => import('@angular/common/locales/en'),
'en-GB': () => import('@angular/common/locales/en-GB'),
'es': () => import('@angular/common/locales/es'),
'de': () => import('@angular/common/locales/de'),
'fi': () => import('@angular/common/locales/fi'),
'fr': () => import('@angular/common/locales/fr'),
'hi': () => import('@angular/common/locales/hi'),
'hu': () => import('@angular/common/locales/hu'),
'is': () => import('@angular/common/locales/is'),
'it': () => import('@angular/common/locales/it'),
'pt': () => import('@angular/common/locales/pt'),
'tr': () => import('@angular/common/locales/tr'),
'ru': () => import('@angular/common/locales/ru'),
'ro': () => import('@angular/common/locales/ro'),
'sk': () => import('@angular/common/locales/sk'),
'sl': () => import('@angular/common/locales/sl'),
'zh-Hans': () => import('@angular/common/locales/zh-Hans'),
'zh-Hant': () => import('@angular/common/locales/zh-Hant')
}
ar: () => import('@angular/common/locales/ar'),
cs: () => import('@angular/common/locales/cs'),
en: () => import('@angular/common/locales/en'),
'en-GB': () => import('@angular/common/locales/en-GB'),
es: () => import('@angular/common/locales/es'),
de: () => import('@angular/common/locales/de'),
fi: () => import('@angular/common/locales/fi'),
fr: () => import('@angular/common/locales/fr'),
hi: () => import('@angular/common/locales/hi'),
hu: () => import('@angular/common/locales/hu'),
is: () => import('@angular/common/locales/is'),
it: () => import('@angular/common/locales/it'),
pt: () => import('@angular/common/locales/pt'),
tr: () => import('@angular/common/locales/tr'),
ru: () => import('@angular/common/locales/ru'),
ro: () => import('@angular/common/locales/ro'),
sk: () => import('@angular/common/locales/sk'),
sl: () => import('@angular/common/locales/sl'),
'zh-Hans': () => import('@angular/common/locales/zh-Hans'),
'zh-Hant': () => import('@angular/common/locales/zh-Hant'),
};
return list[locale]();
}
export function registerLocaleForEsBuild(
{
cultureNameLocaleFileMap = {},
errorHandlerFn = defaultLocalErrorHandlerFn,
cultureNameLocaleFileMap = {},
errorHandlerFn = defaultLocalErrorHandlerFn,
} = {} as RegisterLocaleData,
) {
return (locale: string): Promise<any> => {
localeMap = { ...differentLocales, ...cultureNameLocaleFileMap };
const l = localeMap[locale] || locale;
const localeSupportList = "ar|cs|en|en-GB|es|de|fi|fr|hi|hu|is|it|pt|tr|ru|ro|sk|sl|zh-Hans|zh-Hant".split("|");
localeMap = { ...differentLocales, ...cultureNameLocaleFileMap };
const l = localeMap[locale] || locale;
const localeSupportList =
'ar|cs|en|en-GB|es|de|fi|fr|hi|hu|is|it|pt|tr|ru|ro|sk|sl|zh-Hans|zh-Hant'.split('|');
if (localeSupportList.indexOf(l) == -1) {
return;
}
return new Promise((resolve, reject) => {
return loadLocale(l)
.then(val => {
let module = val;
while (module.default) {
module = module.default;
}
resolve({ default: module });
})
.catch(error => {
errorHandlerFn({
resolve,
reject,
error,
locale,
});
});
});
if (localeSupportList.indexOf(l) == -1) {
return;
}
return new Promise((resolve, reject) => {
return loadLocale(l)
.then(val => {
let module = val;
while (module.default) {
module = module.default;
}
resolve({ default: module });
})
.catch(error => {
errorHandlerFn({
resolve,
reject,
error,
locale,
});
});
});
};
}
export function registerLocale(
{
cultureNameLocaleFileMap = {},
@ -118,6 +117,18 @@ export function registerLocale(
};
}
export function safeRegisterLocale(): (locale: string) => Promise<any> {
return (locale: string) => {
const platformId = inject(PLATFORM_ID);
if (!isPlatformBrowser(platformId)) {
return Promise.resolve({ default: null });
}
// sadece tarayıcıda gerçek locale yükle
return registerLocale()(locale);
};
}
const extraLocales = {} as { [key: string]: any };
export function storeLocaleData(data: any, localeId: string) {
extraLocales[localeId] = data;

2
npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts

@ -26,4 +26,4 @@ export const authGuard: CanActivateFn = () => {
export const asyncAuthGuard: CanActivateFn = () => {
console.error('You should add @abp/ng-oauth packages or create your own auth packages.');
return false;
};
};

2
npm/ng-packs/packages/core/src/lib/abstracts/auth.service.ts

@ -108,5 +108,5 @@ export interface IAuthService {
getAccessToken(): string;
refreshToken(): Promise<AbpAuthResponse>;
refreshToken(): Promise<AbpAuthResponse | void>;
}

3
npm/ng-packs/packages/core/src/lib/core.module.ts

@ -3,6 +3,7 @@ import {
provideHttpClient,
withInterceptorsFromDi,
withXsrfConfiguration,
withFetch,
} from '@angular/common/http';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@ -96,7 +97,7 @@ const CORE_COMPONENTS = [
...CORE_COMPONENTS,
],
declarations: [],
providers: [LocalizationPipe, provideHttpClient(withInterceptorsFromDi())],
providers: [LocalizationPipe, provideHttpClient(withInterceptorsFromDi(), withFetch())],
})
export class BaseCoreModule {}

9
npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts

@ -1,4 +1,4 @@
import { Injectable, inject } from '@angular/core';
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
@ -11,6 +11,7 @@ import { filter, take, tap } from 'rxjs/operators';
import { AuthService, IAbpGuard } from '../abstracts';
import { findRoute, getRoutePath } from '../utils/route-utils';
import { RoutesService, PermissionService, HttpErrorReporterService } from '../services';
import { isPlatformServer } from '@angular/common';
/**
* @deprecated Use `permissionGuard` *function* instead.
*/
@ -57,6 +58,7 @@ export const permissionGuard: CanActivateFn = (
const authService = inject(AuthService);
const permissionService = inject(PermissionService);
const httpErrorReporter = inject(HttpErrorReporterService);
const platformId = inject(PLATFORM_ID);
let { requiredPolicy } = route.data || {};
@ -69,6 +71,11 @@ export const permissionGuard: CanActivateFn = (
return of(true);
}
//TODO enable permission check on ssr
if (isPlatformServer(platformId)) {
return of(true);
}
return permissionService.getGrantedPolicy$(requiredPolicy).pipe(
filter(Boolean),
take(1),

1
npm/ng-packs/packages/core/src/lib/interceptors/index.ts

@ -1,2 +1,3 @@
export * from './api.interceptor';
export * from './timezone.interceptor';
export * from './transfer-state.interceptor';

41
npm/ng-packs/packages/core/src/lib/interceptors/timezone.interceptor.ts

@ -1,26 +1,23 @@
import { inject, Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { inject } from '@angular/core';
import { HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
import { TimezoneService } from '../services';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class TimezoneInterceptor implements HttpInterceptor {
protected readonly timezoneService = inject(TimezoneService);
export const timezoneInterceptor: HttpInterceptorFn = (
req: HttpRequest<any>,
next: HttpHandlerFn,
) => {
const timezoneService = inject(TimezoneService);
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.timezoneService.isUtcClockEnabled) {
return next.handle(req);
}
const timezone = this.timezoneService.timezone;
if (timezone) {
req = req.clone({
setHeaders: {
__timezone: timezone,
},
});
}
return next.handle(req);
if (!timezoneService.isUtcClockEnabled) {
return next(req);
}
}
const timezone = timezoneService.timezone;
if (timezone) {
req = req.clone({
setHeaders: {
__timezone: timezone,
},
});
}
return next(req);
};

42
npm/ng-packs/packages/core/src/lib/interceptors/transfer-state.interceptor.ts

@ -0,0 +1,42 @@
import { inject, makeStateKey, PLATFORM_ID, TransferState } from '@angular/core';
import {
HttpEvent,
HttpHandlerFn,
HttpInterceptorFn,
HttpRequest,
HttpResponse,
} from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { tap } from 'rxjs/operators';
export const transferStateInterceptor: HttpInterceptorFn = (
req: HttpRequest<any>,
next: HttpHandlerFn,
): Observable<HttpEvent<any>> => {
const transferState = inject(TransferState);
const platformId = inject(PLATFORM_ID);
if (req.method !== 'GET') {
return next(req);
}
const stateKey = makeStateKey<HttpResponse<any>>(req.urlWithParams);
if (isPlatformBrowser(platformId)) {
const storedResponse = transferState.get<HttpResponse<any>>(stateKey, null);
if (storedResponse) {
transferState.remove(stateKey);
return of(new HttpResponse<any>({ body: storedResponse, status: 200 }));
}
}
return next(req).pipe(
tap(event => {
if (isPlatformServer(platformId) && event instanceof HttpResponse) {
transferState.set(stateKey, event.body);
console.log(`Interceptor: ${req.urlWithParams} is stored in TransferState.`);
}
}),
);
};

2
npm/ng-packs/packages/core/src/lib/models/environment.ts

@ -7,7 +7,7 @@ export interface Environment {
hmr?: boolean;
test?: boolean;
localization?: { defaultResourceName?: string };
oAuthConfig?: AuthConfig & { impersonation?: Impersonation };
oAuthConfig?: AuthConfig & { impersonation?: Impersonation, ssrAuthorizationUrl?: string };
production: boolean;
remoteEnv?: RemoteEnv;
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any

8
npm/ng-packs/packages/core/src/lib/providers/cookie-language.provider.ts

@ -1,10 +1,14 @@
import { Injector, inject, provideAppInitializer } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Injector, inject, provideAppInitializer, PLATFORM_ID } from '@angular/core';
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import { SessionStateService } from '../services/session-state.service';
import { COOKIE_LANGUAGE_KEY } from '../tokens/cookie-language-key.token';
export function setLanguageToCookie() {
const injector = inject(Injector);
const platformId = injector.get(PLATFORM_ID);
if (!isPlatformBrowser(platformId)) return;
const sessionState = injector.get(SessionStateService);
const document = injector.get(DOCUMENT);
const cookieLanguageKey = injector.get(COOKIE_LANGUAGE_KEY);

12
npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts

@ -1,8 +1,9 @@
import { makeEnvironmentProviders, Provider, provideAppInitializer, inject } from '@angular/core';
import { TitleStrategy } from '@angular/router';
import {
HTTP_INTERCEPTORS,
provideHttpClient,
withFetch,
withInterceptors,
withInterceptorsFromDi,
withXsrfConfiguration,
} from '@angular/common/http';
@ -25,7 +26,7 @@ import { DEFAULT_DYNAMIC_LAYOUTS } from '../constants';
import { LocalizationService, LocalStorageListenerService, AbpTitleStrategy } from '../services';
import { DefaultQueueManager, getInitialData } from '../utils';
import { CookieLanguageProvider, IncludeLocalizationResourcesProvider, LocaleProvider } from './';
import { TimezoneInterceptor } from '../interceptors';
import { timezoneInterceptor, transferStateInterceptor } from '../interceptors';
export enum CoreFeatureKind {
Options,
@ -105,6 +106,8 @@ export function provideAbpCore(...features: CoreFeature<CoreFeatureKind>[]) {
cookieName: 'XSRF-TOKEN',
headerName: 'RequestVerificationToken',
}),
withFetch(),
withInterceptors([transferStateInterceptor, timezoneInterceptor]),
),
provideAppInitializer(async () => {
inject(LocalizationService);
@ -128,11 +131,6 @@ export function provideAbpCore(...features: CoreFeature<CoreFeatureKind>[]) {
provide: TitleStrategy,
useExisting: AbpTitleStrategy,
},
{
provide: HTTP_INTERCEPTORS,
useClass: TimezoneInterceptor,
multi: true,
},
];
for (const feature of features) {

99
npm/ng-packs/packages/core/src/lib/services/cookie-storage.service.ts

@ -0,0 +1,99 @@
import { Injectable, PLATFORM_ID, inject, REQUEST } from '@angular/core';
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class AbpCookieStorageService implements Storage {
private platformId = inject(PLATFORM_ID);
private document = inject(DOCUMENT);
private request = inject(REQUEST);
get length(): number {
return isPlatformBrowser(this.platformId) ? this.keys().length : this.getCookiesFromRequest()?.size ?? 0;
}
clear(): void {
if (!isPlatformBrowser(this.platformId)) return;
this.keys().forEach(k => this.removeItem(k));
}
getItem(key: string): string | null {
if (!isPlatformBrowser(this.platformId)) {
return this.getCookiesFromRequest()?.get(key) ?? null;
}
const name = key + '=';
const parts = (this.document.cookie || '').split('; ');
for (const p of parts) {
if (p.startsWith(name)) {
return decodeURIComponent(p.slice(name.length));
}
}
return null;
}
key(index: number): string | null {
if (!isPlatformBrowser(this.platformId)) return null;
return this.keys()[index] ?? null;
}
removeItem(key: string): void {
if (!isPlatformBrowser(this.platformId)) return;
this.setCookie(key, '', { 'max-age': -1, path: '/' });
}
setItem(key: string, value: string): void {
if (!isPlatformBrowser(this.platformId)) return;
this.setCookie(key, encodeURIComponent(value), {
path: '/',
sameSite: 'Lax',
secure: true,
});
}
setItemWithExpiry(key: string, value: string, seconds: number): void {
if (!isPlatformBrowser(this.platformId)) return;
this.setCookie(key, encodeURIComponent(value), {
path: '/',
sameSite: 'Lax',
secure: true,
'max-age': Math.max(0, Math.floor(seconds)),
});
}
private keys(): string[] {
const raw = (this.document.cookie || '').split('; ').filter(Boolean);
return raw
.map(c => decodeURIComponent(c.split('=')[0]));
}
private setCookie(name: string, value: string, opts: {
path?: string;
domain?: string;
secure?: boolean;
sameSite?: 'Lax' | 'Strict' | 'None';
expires?: Date;
'max-age'?: number;
}) {
let s = `${name}=${value}`;
if (opts.path) s += `; Path=${opts.path}`;
if (opts.domain) s += `; Domain=${opts.domain}`;
if (opts.sameSite) s += `; SameSite=${opts.sameSite}`;
if (opts.secure) s += `; Secure`;
if (opts.expires) s += `; Expires=${opts.expires.toUTCString()}`;
if (typeof opts['max-age'] === 'number') s += `; Max-Age=${opts['max-age']}`;
this.document.cookie = s;
}
private getCookiesFromRequest(): Map<string, string> {
const cookies = new Map<string, string>();
const cookieHeader = this.request?.headers.get('cookie') ?? '';
for (const part of cookieHeader.split(';')) {
const i = part.indexOf('=');
if (i > -1) {
const k = part.slice(0, i).trim();
const v = decodeURIComponent(part.slice(i + 1).trim());
cookies.set(k, v);
}
}
return cookies;
}
}

28
npm/ng-packs/packages/core/src/lib/services/dom-strategy.service.ts

@ -0,0 +1,28 @@
import { inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { DomStrategy } from '../strategies';
@Injectable({ providedIn: 'root' })
export class DomStrategyService {
private document = inject(DOCUMENT);
afterElement(el: HTMLElement) {
return new DomStrategy(() => el, 'afterend');
}
beforeElement(el: HTMLElement) {
return new DomStrategy(() => el, 'beforebegin');
}
appendToBody() {
return new DomStrategy(() => this.document.body, 'beforeend');
}
appendToHead() {
return new DomStrategy(() => this.document.head, 'beforeend');
}
prependToHead() {
return new DomStrategy(() => this.document.head, 'afterbegin');
}
}

2
npm/ng-packs/packages/core/src/lib/services/index.ts

@ -25,3 +25,5 @@ export * from './local-storage-listener.service';
export * from './title-strategy.service';
export * from './timezone.service';
export * from './time.service';
export * from './cookie-storage.service';
export * from './dom-strategy.service';

32
npm/ng-packs/packages/core/src/lib/services/local-storage.service.ts

@ -1,28 +1,44 @@
import { Injectable } from '@angular/core';
import { inject, Injectable, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Injectable({
providedIn: 'root',
})
export class AbpLocalStorageService implements Storage {
constructor() {}
private platformId = inject(PLATFORM_ID);
constructor() {
}
[name: string]: any;
get length(): number {
return localStorage.length;
return isPlatformBrowser(this.platformId) ? localStorage.length : 0;
}
clear(): void {
localStorage.clear();
if (isPlatformBrowser(this.platformId)) {
localStorage.clear();
}
}
getItem(key: string): string {
getItem(key: string): string | null {
if (!isPlatformBrowser(this.platformId)) {
return null;
}
return localStorage.getItem(key);
}
key(index: number): string {
key(index: number): string | null {
if (!isPlatformBrowser(this.platformId)) {
return null;
}
return localStorage.key(index);
}
removeItem(key: string): void {
localStorage.removeItem(key);
if (isPlatformBrowser(this.platformId)) {
localStorage.removeItem(key);
}
}
setItem(key: string, value: string): void {
localStorage.setItem(key, value);
if (isPlatformBrowser(this.platformId)) {
localStorage.setItem(key, value);
}
}
}

16
npm/ng-packs/packages/core/src/lib/services/session-state.service.ts

@ -7,6 +7,8 @@ import { CurrentTenantDto } from '../proxy/volo/abp/asp-net-core/mvc/multi-tenan
import { InternalStore } from '../utils/internal-store-utils';
import { ConfigStateService } from './config-state.service';
import { AbpLocalStorageService } from './local-storage.service';
import { APP_STARTED_WITH_SSR } from '../tokens';
import { AbpCookieStorageService } from './cookie-storage.service';
@Injectable({
providedIn: 'root',
@ -14,12 +16,18 @@ import { AbpLocalStorageService } from './local-storage.service';
export class SessionStateService {
private configState = inject(ConfigStateService);
private localStorageService = inject(AbpLocalStorageService);
private appStartedWithSSR = inject(APP_STARTED_WITH_SSR, { optional: true });
private cookieStorageService = inject(AbpCookieStorageService);
private readonly store = new InternalStore({} as Session.State);
protected readonly document = inject(DOCUMENT);
private updateLocalStorage = () => {
this.localStorageService.setItem('abpSession', JSON.stringify(this.store.state));
if (this.appStartedWithSSR) {
this.cookieStorageService.setItem('abpSession', JSON.stringify(this.store.state));
} else {
this.localStorageService.setItem('abpSession', JSON.stringify(this.store.state));
}
};
constructor() {
@ -28,11 +36,11 @@ export class SessionStateService {
}
private init() {
const session = this.localStorageService.getItem('abpSession');
const storageService = this.appStartedWithSSR ? this.cookieStorageService : this.localStorageService;
const session = storageService.getItem('abpSession');
if (session) {
this.store.set(JSON.parse(session));
}
this.store.sliceUpdate(state => state).subscribe(this.updateLocalStorage);
}
@ -49,7 +57,7 @@ export class SessionStateService {
if (lang.includes(';')) {
lang = lang.split(';')[0];
}
this.setLanguage(lang);
});
}

1
npm/ng-packs/packages/core/src/lib/services/window.service.ts

@ -18,6 +18,7 @@ export class AbpWindowService {
reloadPage(): void {
this.window.location.reload();
}
downloadBlob(blob: Blob, fileName: string) {
const blobUrl = this.window.URL.createObjectURL(blob);
const a = this.document.createElement('a');

6
npm/ng-packs/packages/core/src/lib/strategies/content.strategy.ts

@ -1,5 +1,7 @@
import { CONTENT_SECURITY_STRATEGY, ContentSecurityStrategy } from './content-security.strategy';
import { DOM_STRATEGY, DomStrategy } from './dom.strategy';
import { inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
export type ElementOptions<T extends HTMLScriptElement | HTMLStyleElement = any> = Partial<{
[key in keyof T]: T[key];
@ -35,7 +37,7 @@ export abstract class ContentStrategy<T extends HTMLScriptElement | HTMLStyleEle
export class StyleContentStrategy extends ContentStrategy<HTMLStyleElement> {
createElement(): HTMLStyleElement {
const element = document.createElement('style');
const element = inject(DOCUMENT).createElement('style');
element.textContent = this.content;
return element;
@ -44,7 +46,7 @@ export class StyleContentStrategy extends ContentStrategy<HTMLStyleElement> {
export class ScriptContentStrategy extends ContentStrategy<HTMLScriptElement> {
createElement(): HTMLScriptElement {
const element = document.createElement('script');
const element = inject(DOCUMENT).createElement('script');
element.textContent = this.content;
return element;

17
npm/ng-packs/packages/core/src/lib/strategies/dom.strategy.ts

@ -1,28 +1,31 @@
export class DomStrategy {
constructor(
public target: HTMLElement = document.head,
private getTarget: () => HTMLElement,
public position: InsertPosition = 'beforeend',
) {}
insertElement<T extends HTMLElement>(element: T) {
this.target.insertAdjacentElement(this.position, element);
if (typeof document !== 'undefined') {
const target = this.getTarget();
target.insertAdjacentElement(this.position, element);
}
}
}
export const DOM_STRATEGY = {
AfterElement(element: HTMLElement) {
return new DomStrategy(element, 'afterend');
return new DomStrategy(() => element, 'afterend');
},
AppendToBody() {
return new DomStrategy(document.body, 'beforeend');
return new DomStrategy(() => document?.body, 'beforeend');
},
AppendToHead() {
return new DomStrategy(document.head, 'beforeend');
return new DomStrategy(() => document?.head, 'beforeend');
},
BeforeElement(element: HTMLElement) {
return new DomStrategy(element, 'beforebegin');
return new DomStrategy(() => element, 'beforebegin');
},
PrependToHead() {
return new DomStrategy(document.head, 'afterbegin');
return new DomStrategy(() => document?.head, 'afterbegin');
},
};

1
npm/ng-packs/packages/core/src/lib/tokens/index.ts

@ -17,3 +17,4 @@ export * from './tenant-not-found-by-name';
export * from './compare-func.token';
export * from './dynamic-layout.token';
export * from './title-strategy-disable-project-name.token';
export * from './ssr-state.token';

19
npm/ng-packs/packages/core/src/lib/tokens/ssr-state.token.ts

@ -0,0 +1,19 @@
import { inject, InjectionToken, makeStateKey, PLATFORM_ID, TransferState } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { AbpCookieStorageService } from '../services';
export const SSR_FLAG = makeStateKey<boolean>('SSR_FLAG');
export const APP_STARTED_WITH_SSR = new InjectionToken<boolean>('APP_STARTED_WITH_SSR', {
providedIn: 'root',
factory: () => {
const platformId = inject(PLATFORM_ID);
const cookieService = inject(AbpCookieStorageService);
if (!isPlatformBrowser(platformId)) return true;
const ts = inject(TransferState);
const ssrEnabled = cookieService.getItem('ssr-init');
// Remove the cookie after reading its value because it's only needed once
cookieService.removeItem('ssr-init');
return ts.get(SSR_FLAG, false) || ssrEnabled === 'true';
},
});

1
npm/ng-packs/packages/core/src/lib/utils/index.ts

@ -18,4 +18,5 @@ export * from './queue';
export * from './route-utils';
export * from './string-utils';
export * from './tree-utils';
export * from './server-cookie-utils';
export { NgxValidateCoreModule, Validation } from '@ngx-validate/core';

3
npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts

@ -49,7 +49,8 @@ export async function getInitialData() {
return throwError(() => error);
}),
);
await lastValueFrom(result$);
// TODO: Not working with SSR
// await lastValueFrom(result$);
await localeInitializer(injector);
}

29
npm/ng-packs/packages/core/src/lib/utils/multi-tenancy-utils.ts

@ -1,6 +1,7 @@
import { Injector } from '@angular/core';
import { Injector, PLATFORM_ID } from '@angular/core';
import clone from 'just-clone';
import { Environment } from '../models/environment';
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import { FindTenantResultDto } from '../proxy/volo/abp/asp-net-core/mvc/multi-tenancy/models';
import { EnvironmentService } from '../services/environment.service';
@ -12,16 +13,27 @@ import { HttpErrorResponse } from '@angular/common/http';
const tenancyPlaceholder = '{0}';
function getCurrentTenancyName(appBaseUrl: string): string {
function getCurrentTenancyName(appBaseUrl: string, injector: Injector): string {
const platformId = injector.get(PLATFORM_ID);
const document = injector.get(DOCUMENT);
if (appBaseUrl.charAt(appBaseUrl.length - 1) !== '/') appBaseUrl += '/';
const parseTokens = createTokenParser(appBaseUrl);
const token = tenancyPlaceholder.replace(/[}{]/g, '');
return parseTokens(window.location.href)[token]?.[0];
const tokenValue = isPlatformBrowser(platformId)
? parseTokens(document.defaultView?.location.href)[token]?.[0]
: undefined;
return tokenValue;
}
function getCurrentTenancyNameFromUrl(tenantKey: string) {
const urlParams = new URLSearchParams(window.location.search);
export function getCurrentTenancyNameFromUrl(tenantKey: string, injector): string | null {
const platformId = injector.get(PLATFORM_ID);
const document = injector.get(DOCUMENT);
if (!isPlatformBrowser(platformId)) {
return null;
}
const search = document.defaultView?.location.search;
const urlParams = new URLSearchParams(search);
return urlParams.get(tenantKey);
}
@ -31,7 +43,7 @@ export async function parseTenantFromUrl(injector: Injector) {
const tenantNotFoundHandler = injector.get(TENANT_NOT_FOUND_BY_NAME, null);
const baseUrl = environmentService.getEnvironment()?.application?.baseUrl || '';
const tenancyName = getCurrentTenancyName(baseUrl);
const tenancyName = getCurrentTenancyName(baseUrl, injector);
const hideTenantBox = () => {
multiTenancyService.isTenantBoxVisible = false;
@ -79,7 +91,10 @@ export async function parseTenantFromUrl(injector: Injector) {
*/
replaceTenantNameWithinEnvironment(injector, '', tenancyPlaceholder + '.');
const tenantIdFromQueryParams = getCurrentTenancyNameFromUrl(multiTenancyService.tenantKey);
const tenantIdFromQueryParams = getCurrentTenancyNameFromUrl(
multiTenancyService.tenantKey,
injector,
);
if (tenantIdFromQueryParams) {
const tenantById$ = multiTenancyService.setTenantById(tenantIdFromQueryParams);
return firstValueFrom(tenantById$);

44
npm/ng-packs/packages/core/src/lib/utils/server-cookie-utils.ts

@ -0,0 +1,44 @@
export class ServerCookieParser {
static parse(cookieHeader: string): { [key: string]: string } {
const cookies: { [key: string]: string } = {};
if (!cookieHeader) return cookies;
try {
cookieHeader.split(';').forEach(cookie => {
const parts = cookie.trim().split('=');
if (parts.length >= 2) {
const name = parts[0].trim();
const value = parts.slice(1).join('=');
if (name) {
try {
cookies[name] = decodeURIComponent(value);
} catch (e) {
cookies[name] = value;
}
}
}
});
} catch (error) {
console.error('Error parsing cookies:', error);
}
return cookies;
}
static middleware() {
return (req: any, res: any, next: any) => {
req.cookies = ServerCookieParser.parse(req.headers.cookie || '');
next();
};
}
static getCookie(req: any, name: string): string | undefined {
const cookieHeader = req.headers.cookie;
if (!cookieHeader) return undefined;
const cookies = ServerCookieParser.parse(cookieHeader);
return cookies[name];
}
}

5
npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
import { Component, EventEmitter, Input, Output, inject, DOCUMENT } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ConfigStateService, LocalizationPipe, TrackByService } from '@abp/ng.core';
@ -55,6 +55,7 @@ export class FeatureManagementComponent
protected readonly service = inject(FeaturesService);
protected readonly configState = inject(ConfigStateService);
protected readonly confirmationService = inject(ConfirmationService);
private document = inject(DOCUMENT);
@Input()
providerKey: string;
@ -118,7 +119,7 @@ export class FeatureManagementComponent
this.features = res.groups.reduce(
(acc, val) => ({
...acc,
[val.name]: mapFeatures(val.features, document.body.dir as LocaleDirection),
[val.name]: mapFeatures(val.features, this.document.body?.dir as LocaleDirection),
}),
{},
);

15
npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.ts

@ -4,7 +4,7 @@ import {
LocalizationPipe,
PagedAndSortedResultRequestDto,
PagedResultDto,
ReplaceableTemplateDirective,
ReplaceableTemplateDirective
} from '@abp/ng.core';
import { IdentityRoleDto, IdentityRoleService } from '@abp/ng.identity/proxy';
import {
@ -66,19 +66,12 @@ export class RolesComponent implements OnInit {
protected readonly service = inject(IdentityRoleService);
data: PagedResultDto<IdentityRoleDto> = { items: [], totalCount: 0 };
form!: UntypedFormGroup;
selected?: IdentityRoleDto;
isModalVisible!: boolean;
visiblePermissions = false;
providerKey?: string;
modalBusy = false;
permissionManagementKey = ePermissionManagementComponents.PermissionManagement;
onVisiblePermissionChange = (event: boolean) => {
@ -142,7 +135,11 @@ export class RolesComponent implements OnInit {
}
private hookToQuery() {
this.list.hookToQuery(query => this.service.getList(query)).subscribe(res => (this.data = res));
this.list
.hookToQuery(query => this.service.getList(query))
.subscribe(res => {
this.data = res;
});
}
openPermissionsModal(providerKey: string) {

6
npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts

@ -232,7 +232,11 @@ export class UsersComponent implements OnInit {
}
private hookToQuery() {
this.list.hookToQuery(query => this.service.getList(query)).subscribe(res => (this.data = res));
this.list
.hookToQuery(query => this.service.getList(query))
.subscribe(res => {
this.data = res;
});
}
openPermissionsModal(providerKey: string, entityDisplayName?: string) {

40
npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts

@ -1,15 +1,16 @@
import { Injectable, inject } from '@angular/core';
import { Injectable, inject, PLATFORM_ID, RESPONSE_INIT } from '@angular/core';
import {
UrlTree,
ActivatedRouteSnapshot,
RouterStateSnapshot,
CanActivateFn,
CanActivateFn, Params,
} from '@angular/router';
import { Observable, timer, filter, take, map, firstValueFrom, timeout, catchError, of } from 'rxjs';
import { OAuthService } from 'angular-oauth2-oidc';
import { AuthService, IAbpGuard, EnvironmentService } from '@abp/ng.core';
import { isPlatformServer } from '@angular/common';
/**
* @deprecated Use `abpOAuthGuard` *function* instead.
@ -42,6 +43,9 @@ export const abpOAuthGuard: CanActivateFn = (
) => {
const oAuthService = inject(OAuthService);
const authService = inject(AuthService);
const platformId = inject(PLATFORM_ID);
const resInit = inject(RESPONSE_INIT);
const environmentService = inject(EnvironmentService);
const hasValidAccessToken = oAuthService.hasValidAccessToken();
@ -50,10 +54,30 @@ export const abpOAuthGuard: CanActivateFn = (
}
const params = { returnUrl: state.url };
if (isPlatformServer(platformId) && resInit) {
const ssrAuthorizationUrl = environmentService.getEnvironment().oAuthConfig.ssrAuthorizationUrl;
const url = buildLoginUrl(ssrAuthorizationUrl, params);
const headers = new Headers(resInit.headers);
headers.set('Location', url);
resInit.status = 302;
resInit.statusText = 'Found';
resInit.headers = headers;
return;
}
authService.navigateToLogin(params);
return false;
};
export const buildLoginUrl = (path: string, params?: Params): string => {
if (!params || Object.keys(params).length === 0) return path;
const usp = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v == null) continue;
Array.isArray(v) ? v.forEach(x => usp.append(k, String(x))) : usp.set(k, String(v));
}
return `${path}?${usp.toString()}`;
}
export const asyncAbpOAuthGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
@ -61,6 +85,8 @@ export const asyncAbpOAuthGuard: CanActivateFn = (
const oAuthService = inject(OAuthService);
const authService = inject(AuthService);
const environmentService = inject(EnvironmentService);
const platformId = inject(PLATFORM_ID);
const resInit = inject(RESPONSE_INIT);
const { oAuthConfig } = environmentService.getEnvironment();
@ -72,6 +98,16 @@ export const asyncAbpOAuthGuard: CanActivateFn = (
take(1),
timeout(3000),
catchError(() => {
if (isPlatformServer(platformId) && resInit) {
const ssrAuthorizationUrl = environmentService.getEnvironment().oAuthConfig.ssrAuthorizationUrl;
const url = buildLoginUrl(ssrAuthorizationUrl, { returnUrl: state.url });
const headers = new Headers(resInit.headers);
headers.set('Location', url);
resInit.status = 302;
resInit.statusText = 'Found';
resInit.headers = headers;
return;
}
authService.navigateToLogin({ returnUrl: state.url });
return of(false);
})

15
npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts

@ -6,7 +6,6 @@ import {
ApiInterceptor,
PIPE_TO_LOGIN_FN_KEY,
CHECK_AUTHENTICATION_STATE_FN_KEY,
AbpLocalStorageService,
AuthErrorFilterService,
} from '@abp/ng.core';
import { Provider, makeEnvironmentProviders, inject, provideAppInitializer } from '@angular/core';
@ -15,9 +14,10 @@ import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { AbpOAuthGuard, abpOAuthGuard, asyncAbpOAuthGuard, } from '../guards';
import { OAuthConfigurationHandler } from '../handlers';
import { OAuthApiInterceptor } from '../interceptors';
import { AbpOAuthService, OAuthErrorFilterService } from '../services';
import { pipeToLogin, checkAccessToken } from '../utils';
import { AbpOAuthService, BrowserTokenStorageService, OAuthErrorFilterService } from '../services';
import { pipeToLogin, checkAccessToken, oAuthStorageFactory } from '../utils';
import { NavigateToManageProfileProvider } from './navigate-to-manage-profile.provider';
import { ServerTokenStorageService } from '../services/server-token-storage.service';
export function provideAbpOAuth() {
const providers = [
@ -32,7 +32,7 @@ export function provideAbpOAuth() {
{
provide: authGuard,
useValue: abpOAuthGuard,
},
},
{
provide: asyncAuthGuard,
useValue: asyncAbpOAuthGuard,
@ -59,7 +59,12 @@ export function provideAbpOAuth() {
inject(OAuthConfigurationHandler);
}),
OAuthModule.forRoot().providers as Provider[],
{ provide: OAuthStorage, useClass: AbpLocalStorageService },
ServerTokenStorageService,
BrowserTokenStorageService,
{
provide: OAuthStorage,
useFactory: oAuthStorageFactory,
},
{ provide: AuthErrorFilterService, useExisting: OAuthErrorFilterService },
];

36
npm/ng-packs/packages/oauth/src/lib/services/browser-token-storage.service.ts

@ -0,0 +1,36 @@
import { Injectable } from '@angular/core';
import { OAuthStorage } from 'angular-oauth2-oidc';
@Injectable({
providedIn: 'root',
})
export class BrowserTokenStorageService implements OAuthStorage {
getItem(key: string): string {
return this.readCookie(key);
}
removeItem(key: string): void {
this.removeCookie(key);
}
setItem(key: string, data: string): void {
this.writeCookie(key, data);
}
readCookie(name: string): string | null {
if (typeof document === 'undefined') return null;
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? decodeURIComponent(match[2]) : null;
}
writeCookie(name: string, value: string, days = 7): void {
if (typeof document === 'undefined') return;
const expires = new Date(Date.now() + days * 86400000).toUTCString();
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; Secure; SameSite=Lax`;
}
removeCookie(name: string): void {
if (typeof document === 'undefined') return;
document.cookie = `${name}=; Max-Age=0; path=/;`;
}
}

4
npm/ng-packs/packages/oauth/src/lib/services/index.ts

@ -1,3 +1,5 @@
export * from './oauth.service';
export * from './oauth-error-filter.service';
export * from './remember-me.service'
export * from './remember-me.service';
export * from './browser-token-storage.service';
export * from './server-token-storage.service';

15
npm/ng-packs/packages/oauth/src/lib/services/oauth.service.ts

@ -1,19 +1,23 @@
import { Injectable, Injector, inject } from '@angular/core';
import { Injectable, Injector, inject, PLATFORM_ID, DOCUMENT } from '@angular/core';
import { Params } from '@angular/router';
import { from, Observable, lastValueFrom, EMPTY } from 'rxjs';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { AbpAuthResponse, IAuthService, LoginParams } from '@abp/ng.core';
import { AbpAuthResponse, APP_STARTED_WITH_SSR, IAuthService, LoginParams } from '@abp/ng.core';
import { AuthFlowStrategy } from '../strategies';
import { EnvironmentService } from '@abp/ng.core';
import { AUTH_FLOW_STRATEGY } from '../tokens/auth-flow-strategy';
import { OAuthService } from 'angular-oauth2-oidc';
import { HttpHeaders } from '@angular/common/http';
import { isPlatformBrowser } from '@angular/common';
@Injectable({
providedIn: 'root',
})
export class AbpOAuthService implements IAuthService {
protected injector = inject(Injector);
private appStartedWithSsr = this.injector.get(APP_STARTED_WITH_SSR);
private platformId = this.injector.get(PLATFORM_ID);
private document = this.injector.get(DOCUMENT);
private strategy!: AuthFlowStrategy;
private readonly oAuthService: OAuthService;
@ -57,7 +61,6 @@ export class AbpOAuthService implements IAuthService {
if (!this.strategy) {
return EMPTY;
}
return this.strategy.logout(queryParams);
}
@ -102,7 +105,11 @@ export class AbpOAuthService implements IAuthService {
return this.oAuthService.getAccessToken();
}
refreshToken(): Promise<AbpAuthResponse> {
refreshToken(): Promise<AbpAuthResponse | void> {
if (isPlatformBrowser(this.platformId) && this.appStartedWithSsr) {
this.document.defaultView?.location.replace('/authorize');
return Promise.resolve();
}
return this.oAuthService.refreshToken();
}

81
npm/ng-packs/packages/oauth/src/lib/services/remember-me.service.ts

@ -1,32 +1,49 @@
import { AbpLocalStorageService } from '@abp/ng.core';
import { Injectable, inject } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class RememberMeService {
readonly #rememberMe = 'remember_me';
protected readonly localStorageService = inject(AbpLocalStorageService);
set(remember: boolean) {
this.localStorageService.setItem(this.#rememberMe, JSON.stringify(remember));
}
remove() {
this.localStorageService.removeItem(this.#rememberMe);
}
get() {
return Boolean(JSON.parse(this.localStorageService.getItem(this.#rememberMe) || 'false'));
}
getFromToken(accessToken: string) {
const tokenBody = accessToken.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
try {
const parsedToken = JSON.parse(atob(tokenBody));
return Boolean(parsedToken[this.#rememberMe]);
} catch {
return false;
}
}
}
import {
AbpCookieStorageService,
AbpLocalStorageService,
APP_STARTED_WITH_SSR,
} from '@abp/ng.core';
import { Injectable, inject } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class RememberMeService {
readonly #rememberMe = 'remember_me';
protected readonly localStorageService = inject(AbpLocalStorageService);
protected readonly cookieStorageService = inject(AbpCookieStorageService);
private appStartedWithSsr = inject(APP_STARTED_WITH_SSR, { optional: true });
set(remember: boolean) {
if (this.appStartedWithSsr) {
this.cookieStorageService.setItem(this.#rememberMe, JSON.stringify(remember));
return;
}
this.localStorageService.setItem(this.#rememberMe, JSON.stringify(remember));
}
remove() {
if (this.appStartedWithSsr) {
this.cookieStorageService.removeItem(this.#rememberMe);
return;
}
this.localStorageService.removeItem(this.#rememberMe);
}
get() {
if (this.appStartedWithSsr) {
return Boolean(JSON.parse(this.cookieStorageService.getItem(this.#rememberMe) || 'false'));
}
return Boolean(JSON.parse(this.localStorageService.getItem(this.#rememberMe) || 'false'));
}
getFromToken(accessToken: string) {
const tokenBody = accessToken.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
try {
const parsedToken = JSON.parse(atob(tokenBody));
return Boolean(parsedToken[this.#rememberMe]);
} catch {
return false;
}
}
}

32
npm/ng-packs/packages/oauth/src/lib/services/server-token-storage.service.ts

@ -0,0 +1,32 @@
import { Inject, Injectable, Optional } from '@angular/core';
import { OAuthStorage } from 'angular-oauth2-oidc';
import { REQUEST } from '@angular/core';
@Injectable({ providedIn: null })
export class ServerTokenStorageService implements OAuthStorage {
private cookies = new Map<string, string>();
constructor(@Optional() @Inject(REQUEST) private req: Request | null) {
const cookieHeader = this.req?.headers.get('cookie') ?? '';
for (const part of cookieHeader.split(';')) {
const i = part.indexOf('=');
if (i > -1) {
const k = part.slice(0, i).trim();
const v = decodeURIComponent(part.slice(i + 1).trim());
this.cookies.set(k, v);
}
}
}
getItem(key: string): string {
const fromCookie = this.cookies.get(key);
if (fromCookie) {
return fromCookie;
}
return '';
}
setItem(_k: string, _v: string): void {}
removeItem(_k: string): void {}
}

124
npm/ng-packs/packages/oauth/src/lib/strategies/auth-code-flow-strategy.ts

@ -1,27 +1,41 @@
import { noop } from '@abp/ng.core';
import { DOCUMENT, Injector, PLATFORM_ID } from '@angular/core';
import { APP_STARTED_WITH_SSR, noop } from '@abp/ng.core';
import { Params } from '@angular/router';
import { filter, from, of, take, tap } from 'rxjs';
import { AuthFlowStrategy } from './auth-flow-strategy';
import { isTokenExpired } from '../utils';
import { isPlatformBrowser } from '@angular/common';
export class AuthCodeFlowStrategy extends AuthFlowStrategy {
readonly isInternalAuth = false;
private platformId: object;
protected appStartedWithSSR: boolean;
protected document: Document;
constructor(protected injector: Injector) {
super(injector);
this.platformId = injector.get(PLATFORM_ID);
this.appStartedWithSSR = injector.get(APP_STARTED_WITH_SSR);
this.document = injector.get(DOCUMENT);
}
async init() {
this.checkRememberMeOption();
this.listenToTokenReceived();
return super
.init()
.then(() => this.oAuthService.tryLogin().catch(noop))
.then(() => this.oAuthService.setupAutomaticSilentRefresh());
if (!this.appStartedWithSSR && isPlatformBrowser(this.platformId)) {
return super
.init()
.then(() => this.oAuthService.tryLogin().catch(noop))
.then(() => {
this.oAuthService.setupAutomaticSilentRefresh();
});
}
}
private checkRememberMeOption() {
const accessToken = this.oAuthService.getAccessToken();
const isTokenExpire = isTokenExpired(this.oAuthService.getAccessTokenExpiration());
let rememberMe = this.rememberMeService.get();
if (accessToken && !rememberMe) {
const rememberMeValue = this.rememberMeService.getFromToken(accessToken);
@ -42,64 +56,90 @@ export class AuthCodeFlowStrategy extends AuthFlowStrategy {
}
protected setUICulture() {
const urlParams = new URLSearchParams(window.location.search);
this.configState.uiCultureFromAuthCodeFlow = urlParams.get('ui-culture');
if (isPlatformBrowser(this.platformId)) {
const urlParams = new URLSearchParams(window.location.search);
this.configState.uiCultureFromAuthCodeFlow = urlParams.get('ui-culture');
}
}
protected replaceURLParams() {
const location = this.windowService.window.location;
const history = this.windowService.window.history;
if (isPlatformBrowser(this.platformId)) {
const location = this.windowService.window.location;
const history = this.windowService.window.history;
const query = location.search
.replace(/([?&])iss=[^&]*&?/, '$1')
.replace(/([?&])culture=[^&]*&?/, '$1')
.replace(/([?&])ui-culture=[^&]*&?/, '$1')
.replace(/[?&]+$/, '');
const query = location.search
.replace(/([?&])iss=[^&]*&?/, '$1')
.replace(/([?&])culture=[^&]*&?/, '$1')
.replace(/([?&])ui-culture=[^&]*&?/, '$1')
.replace(/[?&]+$/, '');
const href = location.origin + location.pathname + query + location.hash;
const href = location.origin + location.pathname + query + location.hash;
history.replaceState(null, '', href);
history.replaceState(null, '', href);
}
}
protected listenToTokenReceived() {
this.oAuthService.events
.pipe(
filter(event => event.type === 'token_received'),
tap(() => {
this.setUICulture();
this.replaceURLParams();
}),
take(1),
)
.subscribe();
if (isPlatformBrowser(this.platformId) && !this.appStartedWithSSR) {
this.oAuthService.events
.pipe(
filter(event => event.type === 'token_received'),
tap(() => {
//TODO: Investigate how to handle with ssr
this.setUICulture();
this.replaceURLParams();
}),
take(1),
)
.subscribe();
}
}
navigateToLogin(queryParams?: Params) {
let additionalState = '';
if (queryParams?.returnUrl) {
additionalState = queryParams.returnUrl;
if (isPlatformBrowser(this.platformId)) {
if (this.appStartedWithSSR) {
if (this.document.defaultView) {
this.document.defaultView.location.replace('/authorize');
}
} else {
let additionalState = '';
if (queryParams?.returnUrl) {
additionalState = queryParams.returnUrl;
}
const cultureParams = this.getCultureParams(queryParams);
this.oAuthService.initCodeFlow(additionalState, cultureParams);
}
}
const cultureParams = this.getCultureParams(queryParams);
this.oAuthService.initCodeFlow(additionalState, cultureParams);
}
checkIfInternalAuth(queryParams?: Params) {
this.oAuthService.initCodeFlow('', this.getCultureParams(queryParams));
return false;
if (isPlatformBrowser(this.platformId)) {
this.oAuthService.initCodeFlow('', this.getCultureParams(queryParams));
return false;
}
}
logout(queryParams?: Params) {
this.rememberMeService.remove();
if (queryParams?.noRedirectToLogoutUrl) {
this.router.navigate(['/']);
return from(this.oAuthService.revokeTokenAndLogout(true));
if (this.appStartedWithSSR) {
if (this.document.defaultView) {
this.document.defaultView.location.replace('/logout');
}
} else {
if (queryParams?.noRedirectToLogoutUrl) {
this.router.navigate(['/']);
return from(this.oAuthService.revokeTokenAndLogout(true));
}
return from(this.oAuthService.revokeTokenAndLogout(this.getCultureParams(queryParams)));
}
return from(this.oAuthService.revokeTokenAndLogout(this.getCultureParams(queryParams)));
}
login(queryParams?: Params) {
this.oAuthService.initCodeFlow('', this.getCultureParams(queryParams));
return of(null);
if (isPlatformBrowser(this.platformId)) {
this.oAuthService.initCodeFlow('', this.getCultureParams(queryParams));
return of(null);
}
}
}

12
npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts

@ -23,7 +23,6 @@ import {
} from '@abp/ng.core';
import { clearOAuthStorage } from '../utils/clear-o-auth-storage';
import { oAuthStorage } from '../utils/oauth-storage';
import { OAuthErrorFilterService } from '../services';
import { isTokenExpired } from '../utils';
import { RememberMeService } from '../services/remember-me.service';
@ -74,8 +73,8 @@ export abstract class AuthFlowStrategy {
async init(): Promise<any> {
if (this.oAuthConfig.clientId) {
const shouldClear = shouldStorageClear(this.oAuthConfig.clientId, oAuthStorage);
if (shouldClear) clearOAuthStorage(oAuthStorage);
const shouldClear = shouldStorageClear(this.oAuthConfig.clientId, this.injector);
if (shouldClear) clearOAuthStorage(this.injector);
}
this.oAuthService.configure(this.oAuthConfig);
this.oAuthService.events
@ -123,7 +122,7 @@ export abstract class AuthFlowStrategy {
}
protected refreshToken() {
return this.oAuthService.refreshToken().catch(() => clearOAuthStorage());
return this.oAuthService.refreshToken().catch(() => clearOAuthStorage(this.injector));
}
protected listenToOauthErrors() {
@ -133,7 +132,7 @@ export abstract class AuthFlowStrategy {
tap((err: OAuthErrorEvent) => {
const shouldSkip = this.oAuthErrorFilterService.run(err);
if (!shouldSkip) {
clearOAuthStorage();
clearOAuthStorage(this.injector);
}
}),
switchMap(() => this.configState.refreshAppState()),
@ -142,7 +141,8 @@ export abstract class AuthFlowStrategy {
}
}
function shouldStorageClear(clientId: string, storage: OAuthStorage): boolean {
function shouldStorageClear(clientId: string, injector: Injector): boolean {
const storage = injector.get(OAuthStorage);
const key = 'abpOAuthClientId';
if (!storage.getItem(key)) {
storage.setItem(key, clientId);

7
npm/ng-packs/packages/oauth/src/lib/strategies/auth-password-flow-strategy.ts

@ -15,9 +15,10 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
this.oAuthService.events
.pipe(
filter(
event => event instanceof OAuthInfoEvent &&
event =>
event instanceof OAuthInfoEvent &&
event.type === 'token_expires' &&
event.info === 'access_token'
event.info === 'access_token',
),
)
.subscribe(() => {
@ -82,7 +83,7 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
protected refreshToken() {
return this.oAuthService.refreshToken().catch(() => {
clearOAuthStorage();
clearOAuthStorage(this.injector);
this.rememberMeService.remove();
});
}

2
npm/ng-packs/packages/oauth/src/lib/utils/check-access-token.ts

@ -7,6 +7,6 @@ export const checkAccessToken: CheckAuthenticationStateFn = function (injector:
const configState = injector.get(ConfigStateService);
const oAuth = injector.get(OAuthService);
if (oAuth.hasValidAccessToken() && !configState.getDeep('currentUser.id')) {
clearOAuthStorage();
clearOAuthStorage(this.injector);
}
};

5
npm/ng-packs/packages/oauth/src/lib/utils/clear-o-auth-storage.ts

@ -1,7 +1,8 @@
import { OAuthStorage } from 'angular-oauth2-oidc';
import { oAuthStorage } from './oauth-storage';
import { Injector } from '@angular/core';
export function clearOAuthStorage(storage: OAuthStorage = oAuthStorage) {
export function clearOAuthStorage(injector: Injector) {
const storage = injector.get(OAuthStorage);
const keys = [
'access_token',
'id_token',

1
npm/ng-packs/packages/oauth/src/lib/utils/index.ts

@ -1,4 +1,3 @@
export * from './oauth-storage';
export * from './storage.factory';
export * from './auth-utils';
export * from './clear-o-auth-storage';

39
npm/ng-packs/packages/oauth/src/lib/utils/storage.factory.ts

@ -1,6 +1,39 @@
import { inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { ServerTokenStorageService } from '../services/server-token-storage.service';
import { BrowserTokenStorageService } from '../services';
import { OAuthStorage } from 'angular-oauth2-oidc';
import { oAuthStorage } from './oauth-storage';
import { AbpLocalStorageService, APP_STARTED_WITH_SSR } from '@abp/ng.core';
export function storageFactory(): OAuthStorage {
return oAuthStorage;
export class MockStorage implements Storage {
private data = new Map<string, string>();
get length() {
return this.data.size;
}
clear() {
this.data.clear();
}
getItem(key: string) {
return this.data.get(key) || null;
}
key(index: number) {
return Array.from(this.data.keys())[index] || null;
}
removeItem(key: string) {
this.data.delete(key);
}
setItem(key: string, value: string) {
this.data.set(key, value);
}
}
export function oAuthStorageFactory(): OAuthStorage {
const platformId = inject(PLATFORM_ID);
const appStartedWithSSR = inject(APP_STARTED_WITH_SSR);
if (appStartedWithSSR) {
return isPlatformBrowser(platformId)
? inject(BrowserTokenStorageService)
: inject(ServerTokenStorageService);
}
return inject(AbpLocalStorageService);
}

8
npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts

@ -17,6 +17,7 @@ import {
import {
Component,
computed,
DOCUMENT,
ElementRef,
EventEmitter,
inject,
@ -109,6 +110,7 @@ export class PermissionManagementComponent
protected readonly service = inject(PermissionsService);
protected readonly configState = inject(ConfigStateService);
protected readonly toasterService = inject(ToasterService);
private document = inject(DOCUMENT);
@Input()
readonly providerName!: string;
@ -220,7 +222,7 @@ export class PermissionManagementComponent
}
const margin = `margin-${
(document.body.dir as LocaleDirection) === 'rtl' ? 'right' : 'left'
(this.document.body?.dir as LocaleDirection) === 'rtl' ? 'right' : 'left'
}.px`;
const permissions =
@ -344,7 +346,7 @@ export class PermissionManagementComponent
);
const selectedPermissions = selectablePermissions.filter(per => per.isGranted);
const element = document.querySelector('#select-all-in-this-tabs') as any;
const element = this.document.querySelector('#select-all-in-this-tabs') as any;
if (!element) {
return;
}
@ -365,7 +367,7 @@ export class PermissionManagementComponent
per.grantedProviders.every(p => p.providerName === this.providerName),
);
const selectedAllPermissions = selectablePermissions.filter(per => per.isGranted);
const checkboxElement = document.querySelector('#select-all-in-all-tabs') as any;
const checkboxElement = this.document.querySelector('#select-all-in-all-tabs') as any;
if (selectedAllPermissions.length === selectablePermissions.length) {
checkboxElement.indeterminate = false;

2
npm/ng-packs/packages/setting-management/config/src/lib/components/email-setting-group/email-setting-group.component.ts

@ -22,7 +22,7 @@ import {
ConfigStateService,
LocalizationPipe,
LocalizationService,
PermissionDirective,
PermissionDirective
} from '@abp/ng.core';
import { NgxValidateCoreModule } from '@ngx-validate/core';
import { CommonModule } from '@angular/common';

6
npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.ts

@ -24,7 +24,7 @@ import {
FormPropData,
generateFormFromProps,
} from '@abp/ng.components/extensible';
import { Component, inject, Injector, OnInit } from '@angular/core';
import { Component, DOCUMENT, inject, Injector, makeStateKey, OnInit } from '@angular/core';
import {
FormsModule,
ReactiveFormsModule,
@ -68,6 +68,7 @@ export class TenantsComponent implements OnInit {
protected readonly toasterService = inject(ToasterService);
private readonly fb = inject(UntypedFormBuilder);
private readonly injector = inject(Injector);
private document = inject(DOCUMENT);
data: PagedResultDto<TenantDto> = { items: [], totalCount: 0 };
@ -84,6 +85,7 @@ export class TenantsComponent implements OnInit {
modalBusy = false;
featureManagementKey = eFeatureManagementComponents.FeatureManagement;
TENANTS_KEY = makeStateKey<PagedResultDto<TenantDto>>('tenants');
get hasSelectedTenant(): boolean {
return Boolean(this.selected.id);
@ -162,7 +164,7 @@ export class TenantsComponent implements OnInit {
onSharedDatabaseChange(value: boolean) {
if (!value) {
setTimeout(() => {
const defaultConnectionString = document.getElementById(
const defaultConnectionString = this.document.getElementById(
'defaultConnectionString',
) as HTMLInputElement;
if (defaultConnectionString) {

6
npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts

@ -11,7 +11,7 @@ import {
import { AbpVisibleDirective, UserMenu, UserMenuService } from '@abp/ng.theme.shared';
import { Component, TrackByFunction, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { NgComponentOutlet, AsyncPipe } from '@angular/common';
import { NgComponentOutlet, AsyncPipe, DOCUMENT } from '@angular/common';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
@Component({
@ -33,14 +33,14 @@ export class CurrentUserComponent {
private authService = inject(AuthService);
private configState = inject(ConfigStateService);
private sessionState = inject(SessionStateService);
private document = inject(DOCUMENT);
currentUser$: Observable<CurrentUserDto> = this.configState.getOne$('currentUser');
selectedTenant$ = this.sessionState.getTenant$();
trackByFn: TrackByFunction<UserMenu> = (_, element) => element.id;
get smallScreen(): boolean {
return window.innerWidth < 992;
return this.document.defaultView?.innerWidth < 992;
}
navigateToLogin() {

5
npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts

@ -1,8 +1,8 @@
import { ConfigStateService, LanguageInfo, SessionStateService } from '@abp/ng.core';
import { Component, inject } from '@angular/core';
import { DOCUMENT, AsyncPipe } from '@angular/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AsyncPipe } from '@angular/common';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
@Component({
@ -44,9 +44,10 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
export class LanguagesComponent {
private sessionState = inject(SessionStateService);
private configState = inject(ConfigStateService);
document = inject(DOCUMENT);
get smallScreen(): boolean {
return window.innerWidth < 992;
return this.document.defaultView.innerWidth < 992;
}
languages$: Observable<LanguageInfo[]> = this.configState.getDeep$('localization.languages');

28
npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.html

@ -1,16 +1,18 @@
<ul class="navbar-nav">
@for (item of navItems.items$ | async; track $index) {
<ng-container *abpVisible="!item.visible || item.visible(item)">
<li class="nav-item d-flex align-items-center" *abpPermission="item.requiredPolicy">
@if (item.component) {
<ng-container
[ngComponentOutlet]="item.component"
[ngComponentOutletInjector]="item | toInjector"
></ng-container>
} @else {
<div [innerHTML]="item.html" (click)="item.action ? item.action() : null"></div>
}
</li>
</ng-container>
@if (isBrowser) {
@for (item of navItems.items$ | async; track $index) {
<ng-container *abpVisible="!item.visible || item.visible(item)">
<li class="nav-item d-flex align-items-center" *abpPermission="item.requiredPolicy">
@if (item.component) {
<ng-container
[ngComponentOutlet]="item.component"
[ngComponentOutletInjector]="item | toInjector"
></ng-container>
} @else {
<div [innerHTML]="item.html" (click)="item.action ? item.action() : null"></div>
}
</li>
</ng-container>
}
}
</ul>

6
npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts

@ -1,6 +1,6 @@
import { AbpVisibleDirective, NavItem, NavItemsService } from '@abp/ng.theme.shared';
import { Component, TrackByFunction, inject } from '@angular/core';
import { NgComponentOutlet, AsyncPipe } from '@angular/common';
import { Component, TrackByFunction, inject, PLATFORM_ID } from '@angular/core';
import { NgComponentOutlet, AsyncPipe, isPlatformBrowser } from '@angular/common';
import { PermissionDirective, ToInjectorPipe } from '@abp/ng.core';
@Component({
@ -10,6 +10,8 @@ import { PermissionDirective, ToInjectorPipe } from '@abp/ng.core';
})
export class NavItemsComponent {
readonly navItems = inject(NavItemsService);
private platformId = inject(PLATFORM_ID);
readonly isBrowser = isPlatformBrowser(this.platformId);
trackByFn: TrackByFunction<NavItem> = (_, element) => element.id;
}

6
npm/ng-packs/packages/theme-basic/src/lib/handlers/lazy-style.handler.ts

@ -2,10 +2,12 @@ import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { DocumentDirHandlerService, LocaleDirection } from '@abp/ng.theme.shared';
import { Injectable, Injector, inject } from '@angular/core';
import { LAZY_STYLES } from '../tokens/lazy-styles.token';
import { DOCUMENT } from '@angular/common';
export const BOOTSTRAP = 'bootstrap-{{dir}}.min.css';
@Injectable()
export class LazyStyleHandler {
private document = inject(DOCUMENT);
private lazyLoad!: LazyLoadService;
private styles!: string[];
private _dir: LocaleDirection = 'ltr';
@ -34,7 +36,7 @@ export class LazyStyleHandler {
private getHrefFromLink(link: HTMLLinkElement | null | undefined): string {
if (!link) return '';
const a = document.createElement('a');
const a = this.document.createElement('a');
a.href = link.href;
return a.pathname.replace(/^\//, '');
}
@ -42,7 +44,7 @@ export class LazyStyleHandler {
private getLoadedBootstrap(): LoadedStyle {
const href = createLazyStyleHref(BOOTSTRAP, this.dir);
const selector = `[href*="${href.replace(/\.css$/, '')}"]`;
const link = document.querySelector<HTMLLinkElement>(selector);
const link = this.document.querySelector<HTMLLinkElement>(selector);
return { href, link };
}

6
npm/ng-packs/packages/theme-basic/src/lib/services/layout.service.ts

@ -3,12 +3,14 @@ import { ChangeDetectorRef, Injectable, inject } from '@angular/core';
import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { eThemeBasicComponents } from '../enums';
import { DOCUMENT } from '@angular/common';
@Injectable()
export class LayoutService {
private subscription = inject(SubscriptionService);
private cdRef = inject(ChangeDetectorRef);
document = inject(DOCUMENT);
isCollapsed = true;
smallScreen!: boolean; // do not set true or false
@ -29,7 +31,7 @@ export class LayoutService {
}
private checkWindowWidth() {
const isSmallScreen = window.innerWidth < 992;
const isSmallScreen = this.document.defaultView.innerWidth < 992;
if (isSmallScreen && this.smallScreen === false) {
this.isCollapsed = false;
setTimeout(() => {
@ -43,7 +45,7 @@ export class LayoutService {
subscribeWindowSize() {
this.checkWindowWidth();
const resize$ = fromEvent(window, 'resize').pipe(debounceTime(150));
const resize$ = fromEvent(this.document.defaultView, 'resize').pipe(debounceTime(150));
this.subscription.addOne(resize$, () => this.checkWindowWidth());
}
}

5
npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts

@ -12,7 +12,7 @@ import {
output,
viewChild,
} from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { SubscriptionService, uuid } from '@abp/ng.core';
import { NgbModal, NgbModalOptions, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
@ -41,6 +41,7 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
optional: true,
});
protected readonly destroyRef = inject(DestroyRef);
private document = inject(DOCUMENT);
visible = model<boolean>(false);
@ -80,7 +81,7 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
modalIdentifier = `modal-${uuid()}`;
get modalWindowRef() {
return document.querySelector(`ngb-modal-window.${this.modalIdentifier}`);
return this.document.querySelector(`ngb-modal-window.${this.modalIdentifier}`);
}
get isFormDirty(): boolean {

49
npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts

@ -1,5 +1,13 @@
import { DOCUMENT } from '@angular/common';
import { AfterViewInit, Directive, HostBinding, Input, OnDestroy, inject } from '@angular/core';
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import {
AfterViewInit,
Directive,
HostBinding,
Input,
OnDestroy,
inject,
PLATFORM_ID,
} from '@angular/core';
import { ColumnMode, DatatableComponent, ScrollerComponent } from '@swimlane/ngx-datatable';
import { fromEvent, Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@ -12,6 +20,7 @@ import { debounceTime } from 'rxjs/operators';
export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy {
private table = inject(DatatableComponent);
private document = inject<MockDocument>(DOCUMENT);
private platformId = inject(PLATFORM_ID);
private subscription = new Subscription();
private resizeDiff = 0;
@ -34,31 +43,33 @@ export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy {
private fixHorizontalGap(scroller: ScrollerComponent) {
const { body, documentElement } = this.document;
if (documentElement.scrollHeight !== documentElement.clientHeight) {
if (this.resizeDiff === 0) {
this.resizeDiff = window.innerWidth - body.offsetWidth;
scroller.scrollWidth -= this.resizeDiff;
if (isPlatformBrowser(this.platformId)) {
if (documentElement.scrollHeight !== documentElement.clientHeight) {
if (this.resizeDiff === 0) {
this.resizeDiff = window.innerWidth - body.offsetWidth;
scroller.scrollWidth -= this.resizeDiff;
}
} else {
scroller.scrollWidth += this.resizeDiff;
this.resizeDiff = 0;
}
} else {
scroller.scrollWidth += this.resizeDiff;
this.resizeDiff = 0;
}
}
private fixStyleOnWindowResize() {
// avoided @HostListener('window:resize') in favor of performance
const subscription = fromEvent(window, 'resize')
.pipe(debounceTime(500))
.subscribe(() => {
const { scroller } = this.table.bodyComponent;
if (isPlatformBrowser(this.platformId)) {
const subscription = fromEvent(window, 'resize')
.pipe(debounceTime(500))
.subscribe(() => {
const { scroller } = this.table.bodyComponent;
if (!scroller) return;
if (!scroller) return;
this.fixHorizontalGap(scroller);
});
this.fixHorizontalGap(scroller);
});
this.subscription.add(subscription);
this.subscription.add(subscription);
}
}
ngAfterViewInit() {

5
npm/ng-packs/packages/theme-shared/src/lib/handlers/document-dir.handler.ts

@ -3,6 +3,7 @@ import { Injectable, Injector, inject } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
import { LocaleDirection } from '../models/common';
import { DOCUMENT } from '@angular/common';
@Injectable()
export class DocumentDirHandlerService {
@ -24,7 +25,7 @@ export class DocumentDirHandlerService {
}
private setBodyDir(dir: LocaleDirection) {
document.body.dir = dir;
document.dir = dir;
this.injector.get(DOCUMENT).body.dir = dir;
this.injector.get(DOCUMENT).dir = dir;
}
}

2
npm/ng-packs/packages/theme-shared/src/lib/providers/tenant-not-found.provider.ts

@ -2,11 +2,13 @@ import { TENANT_NOT_FOUND_BY_NAME } from '@abp/ng.core';
import { inject, Provider } from '@angular/core';
import { ConfirmationService } from '../services';
import { HttpErrorResponse } from '@angular/common/http';
import { DOCUMENT } from '@angular/common';
export const tenantNotFoundProvider: Provider = {
provide: TENANT_NOT_FOUND_BY_NAME,
useFactory: function () {
const confirm = inject(ConfirmationService);
const document = inject(DOCUMENT);
return (response: HttpErrorResponse) => {
const { error } = response.error;
// hide loading donut

4
npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts

@ -4,10 +4,12 @@ import { fromEvent, Observable, ReplaySubject, Subject } from 'rxjs';
import { debounceTime, filter, takeUntil } from 'rxjs/operators';
import { ConfirmationComponent } from '../components/confirmation/confirmation.component';
import { Confirmation } from '../models/confirmation';
import { DOCUMENT } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class ConfirmationService {
private contentProjectionService = inject(ContentProjectionService);
private document = inject(DOCUMENT);
status$!: Subject<Confirmation.Status>;
confirmation$ = new ReplaySubject<Confirmation.DialogData | null>(1);
@ -87,7 +89,7 @@ export class ConfirmationService {
}
private listenToEscape() {
fromEvent<KeyboardEvent>(document, 'keyup')
fromEvent<KeyboardEvent>(this.document, 'keyup')
.pipe(
takeUntil(this.status$),
debounceTime(150),

8
templates/app-nolayers/angular/angular.json

@ -19,7 +19,7 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/MyProjectName",
"index": "src/index.html",
@ -145,7 +145,7 @@
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "MyProjectName:build:production"
@ -157,13 +157,13 @@
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "MyProjectName:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"builder": "@angular/build:karma",
"options": {
"browser": "src/test.ts",
"polyfills": ["src/polyfills.ts"],

2
templates/app-nolayers/angular/karma.conf.js

@ -10,7 +10,7 @@ module.exports = function (config) {
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {

1
templates/app-nolayers/angular/package.json

@ -46,6 +46,7 @@
"@angular/cli": "~20.0.0",
"@angular/compiler-cli": "~20.0.0",
"@angular/language-service": "~20.0.0",
"@angular/build": "~20.0.0",
"@types/jasmine": "~3.6.0",
"@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "7.16.0",

1
templates/app-nolayers/angular/tsconfig.json

@ -6,7 +6,6 @@
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,

8
templates/app/angular/angular.json

@ -19,7 +19,7 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/MyProjectName",
"index": "src/index.html",
@ -145,7 +145,7 @@
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "MyProjectName:build:production"
@ -157,13 +157,13 @@
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "MyProjectName:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"builder": "@angular/build:karma",
"options": {
"browser": "src/test.ts",
"polyfills": ["src/polyfills.ts"],

2
templates/app/angular/karma.conf.js

@ -10,7 +10,7 @@ module.exports = function (config) {
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {

3
templates/app/angular/package.json

@ -46,8 +46,9 @@
"@angular/cli": "~20.0.0",
"@angular/compiler-cli": "~20.0.0",
"@angular/language-service": "~20.0.0",
"@angular/build": "~20.0.0",
"@types/jasmine": "~3.6.0",
"@types/node": "^12.11.1",
"@types/node": "~20.11.0",
"@typescript-eslint/eslint-plugin": "7.16.0",
"@typescript-eslint/parser": "7.16.0",
"eslint": "^8.0.0",

1
templates/app/angular/tsconfig.json

@ -6,7 +6,6 @@
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,

12
templates/module/angular/angular.json

@ -13,7 +13,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"builder": "@angular/build:ng-packagr",
"options": {
"project": "projects/my-project-name/ng-package.json"
},
@ -28,7 +28,7 @@
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"builder": "@angular/build:karma",
"options": {
"main": "projects/my-project-name/src/test.ts",
"tsConfig": "projects/my-project-name/tsconfig.spec.json",
@ -58,7 +58,7 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/dev-app",
"index": "projects/dev-app/src/index.html",
@ -139,7 +139,7 @@
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "dev-app:build:production"
@ -151,13 +151,13 @@
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "dev-app:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"builder": "@angular/build:karma",
"options": {
"browser": "projects/dev-app/src/test.ts",
"polyfills": ["projects/dev-app/src/polyfills.ts"],

1
templates/module/angular/package.json

@ -46,6 +46,7 @@
"@angular/cli": "~20.0.0",
"@angular/compiler-cli": "~20.0.0",
"@angular/language-service": "~20.0.0",
"@angular/build": "~20.0.0",
"@types/jasmine": "~3.6.0",
"@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "7.16.0",

2
templates/module/angular/projects/dev-app/karma.conf.js

@ -10,7 +10,7 @@ module.exports = function (config) {
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {

2
templates/module/angular/projects/my-project-name/karma.conf.js

@ -10,7 +10,7 @@ module.exports = function (config) {
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {

1
templates/module/angular/tsconfig.json

@ -1,6 +1,7 @@
{
"extends": "./tsconfig.prod.json",
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"paths": {
"@my-company-name/my-project-name": [

Loading…
Cancel
Save