@memberjunction/ng-auth-services 2.42.1 → 2.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -103
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
# @memberjunction/ng-auth-services
|
|
2
2
|
|
|
3
|
-
The `@memberjunction/ng-auth-services` package provides authentication services for MemberJunction Explorer applications. It offers
|
|
3
|
+
The `@memberjunction/ng-auth-services` package provides authentication services for MemberJunction Explorer Angular applications. It offers a unified abstraction layer that supports multiple authentication providers including Auth0 and Microsoft Authentication Library (MSAL) for Azure Active Directory.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package implements a provider pattern that allows seamless switching between different authentication services through configuration. It provides a consistent API regardless of which authentication provider is being used, making it easy to change authentication strategies without modifying application code.
|
|
4
8
|
|
|
5
9
|
## Features
|
|
6
10
|
|
|
7
|
-
- Unified
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- Reactive
|
|
11
|
+
- **Unified Authentication Interface**: Abstract base class (`MJAuthBase`) provides consistent API across providers
|
|
12
|
+
- **Multiple Provider Support**:
|
|
13
|
+
- Auth0 authentication provider
|
|
14
|
+
- Microsoft Authentication Library (MSAL) for Azure AD
|
|
15
|
+
- **Easy Provider Switching**: Change providers via configuration without code changes
|
|
16
|
+
- **Reactive State Management**: Observable-based authentication state and user information
|
|
17
|
+
- **Token Management**: Built-in token refresh and expiration handling
|
|
18
|
+
- **TypeScript Support**: Full TypeScript definitions for type safety
|
|
19
|
+
- **Angular 18+ Compatible**: Built for modern Angular applications
|
|
13
20
|
|
|
14
21
|
## Installation
|
|
15
22
|
|
|
@@ -19,47 +26,65 @@ npm install @memberjunction/ng-auth-services
|
|
|
19
26
|
|
|
20
27
|
## Requirements
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
29
|
+
### Peer Dependencies
|
|
30
|
+
- `@angular/common`: ^18.0.2
|
|
31
|
+
- `@angular/core`: ^18.0.2
|
|
32
|
+
- `@angular/forms`: ^18.0.2
|
|
33
|
+
- `@angular/router`: ^18.0.2
|
|
34
|
+
- `@auth0/auth0-angular`: ^2.2.1 (required when using Auth0)
|
|
35
|
+
- `@azure/msal-angular`: ^3.0.11 (required when using MSAL)
|
|
36
|
+
|
|
37
|
+
### Dependencies
|
|
38
|
+
- `@memberjunction/core`: ^2.43.0
|
|
39
|
+
- `tslib`: ^2.3.0
|
|
26
40
|
|
|
27
|
-
##
|
|
41
|
+
## Configuration
|
|
28
42
|
|
|
29
|
-
### Setup
|
|
43
|
+
### Environment Setup
|
|
30
44
|
|
|
31
|
-
|
|
45
|
+
Configure your authentication provider in your environment files:
|
|
32
46
|
|
|
47
|
+
#### Auth0 Configuration
|
|
33
48
|
```typescript
|
|
34
49
|
// environment.ts
|
|
35
50
|
export const environment = {
|
|
36
|
-
// For Auth0
|
|
37
51
|
AUTH_TYPE: 'auth0',
|
|
38
|
-
AUTH0_DOMAIN: 'your-
|
|
52
|
+
AUTH0_DOMAIN: 'your-domain.auth0.com',
|
|
39
53
|
AUTH0_CLIENTID: 'your-auth0-client-id',
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
// Other environment variables...
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### MSAL (Azure AD) Configuration
|
|
59
|
+
```typescript
|
|
60
|
+
// environment.ts
|
|
61
|
+
export const environment = {
|
|
62
|
+
AUTH_TYPE: 'msal',
|
|
63
|
+
CLIENT_ID: 'your-azure-ad-client-id',
|
|
64
|
+
CLIENT_AUTHORITY: 'https://login.microsoftonline.com/your-tenant-id',
|
|
65
|
+
// Other environment variables...
|
|
45
66
|
};
|
|
46
67
|
```
|
|
47
68
|
|
|
48
|
-
|
|
69
|
+
### Module Setup
|
|
70
|
+
|
|
71
|
+
Import and configure the `AuthServicesModule` in your app module:
|
|
49
72
|
|
|
50
73
|
```typescript
|
|
51
|
-
import {
|
|
74
|
+
import { NgModule } from '@angular/core';
|
|
75
|
+
import { BrowserModule } from '@angular/platform-browser';
|
|
76
|
+
import { AuthServicesModule } from '@memberjunction/ng-auth-services';
|
|
52
77
|
import { environment } from '../environments/environment';
|
|
78
|
+
import { AppComponent } from './app.component';
|
|
53
79
|
|
|
54
80
|
@NgModule({
|
|
55
81
|
declarations: [
|
|
56
|
-
AppComponent
|
|
57
|
-
// other components
|
|
82
|
+
AppComponent
|
|
58
83
|
],
|
|
59
84
|
imports: [
|
|
60
85
|
BrowserModule,
|
|
61
|
-
//
|
|
62
|
-
|
|
86
|
+
AuthServicesModule.forRoot(environment), // Configure auth module
|
|
87
|
+
// Other imports...
|
|
63
88
|
],
|
|
64
89
|
providers: [],
|
|
65
90
|
bootstrap: [AppComponent]
|
|
@@ -67,99 +92,152 @@ import { environment } from '../environments/environment';
|
|
|
67
92
|
export class AppModule { }
|
|
68
93
|
```
|
|
69
94
|
|
|
70
|
-
|
|
95
|
+
### Routing Configuration (MSAL only)
|
|
96
|
+
|
|
97
|
+
When using MSAL, add the redirect component to your routes:
|
|
71
98
|
|
|
72
99
|
```typescript
|
|
100
|
+
import { Routes } from '@angular/router';
|
|
73
101
|
import { RedirectComponent } from '@memberjunction/ng-auth-services';
|
|
74
102
|
|
|
75
103
|
const routes: Routes = [
|
|
76
|
-
// Your
|
|
77
|
-
{ path: 'auth', component: RedirectComponent }
|
|
104
|
+
// Your application routes...
|
|
105
|
+
{ path: 'auth', component: RedirectComponent } // Required for MSAL
|
|
78
106
|
];
|
|
79
107
|
```
|
|
80
108
|
|
|
81
|
-
|
|
109
|
+
## Usage Examples
|
|
82
110
|
|
|
83
|
-
|
|
111
|
+
### Basic Authentication Operations
|
|
84
112
|
|
|
85
113
|
```typescript
|
|
86
114
|
import { Component, OnInit } from '@angular/core';
|
|
87
115
|
import { MJAuthBase } from '@memberjunction/ng-auth-services';
|
|
116
|
+
import { Observable } from 'rxjs';
|
|
88
117
|
|
|
89
118
|
@Component({
|
|
90
|
-
selector: 'app-
|
|
91
|
-
|
|
119
|
+
selector: 'app-header',
|
|
120
|
+
template: `
|
|
121
|
+
<div class="header">
|
|
122
|
+
<button *ngIf="!(isAuthenticated$ | async)" (click)="login()">Login</button>
|
|
123
|
+
<button *ngIf="isAuthenticated$ | async" (click)="logout()">Logout</button>
|
|
124
|
+
<span *ngIf="user$ | async as user">Welcome, {{ user.name }}!</span>
|
|
125
|
+
</div>
|
|
126
|
+
`
|
|
92
127
|
})
|
|
93
|
-
export class
|
|
94
|
-
|
|
95
|
-
|
|
128
|
+
export class HeaderComponent implements OnInit {
|
|
129
|
+
isAuthenticated$!: Observable<boolean>;
|
|
130
|
+
user$!: Observable<any>;
|
|
131
|
+
|
|
96
132
|
constructor(private authService: MJAuthBase) {}
|
|
97
|
-
|
|
133
|
+
|
|
98
134
|
async ngOnInit() {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
isAuthenticated.subscribe(authenticated => {
|
|
102
|
-
if (authenticated) {
|
|
103
|
-
this.loadUserProfile();
|
|
104
|
-
}
|
|
105
|
-
});
|
|
135
|
+
this.isAuthenticated$ = await this.authService.isAuthenticated();
|
|
136
|
+
this.user$ = await this.authService.getUser();
|
|
106
137
|
}
|
|
107
|
-
|
|
108
|
-
async loadUserProfile() {
|
|
109
|
-
const userObs = await this.authService.getUser();
|
|
110
|
-
userObs.subscribe(user => {
|
|
111
|
-
this.user = user;
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
138
|
+
|
|
115
139
|
login() {
|
|
116
140
|
this.authService.login();
|
|
117
141
|
}
|
|
118
|
-
|
|
142
|
+
|
|
119
143
|
logout() {
|
|
120
144
|
this.authService.logout();
|
|
121
145
|
}
|
|
122
146
|
}
|
|
123
147
|
```
|
|
124
148
|
|
|
125
|
-
###
|
|
149
|
+
### Getting User Claims
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { Component, OnInit } from '@angular/core';
|
|
153
|
+
import { MJAuthBase } from '@memberjunction/ng-auth-services';
|
|
154
|
+
import { Observable } from 'rxjs';
|
|
155
|
+
import { map } from 'rxjs/operators';
|
|
156
|
+
|
|
157
|
+
@Component({
|
|
158
|
+
selector: 'app-user-profile',
|
|
159
|
+
template: `
|
|
160
|
+
<div *ngIf="userClaims$ | async as claims">
|
|
161
|
+
<h3>User Profile</h3>
|
|
162
|
+
<p>Email: {{ claims.email }}</p>
|
|
163
|
+
<p>Name: {{ claims.name }}</p>
|
|
164
|
+
<p>Roles: {{ claims.roles?.join(', ') }}</p>
|
|
165
|
+
</div>
|
|
166
|
+
`
|
|
167
|
+
})
|
|
168
|
+
export class UserProfileComponent implements OnInit {
|
|
169
|
+
userClaims$!: Observable<any>;
|
|
170
|
+
|
|
171
|
+
constructor(private authService: MJAuthBase) {}
|
|
172
|
+
|
|
173
|
+
async ngOnInit() {
|
|
174
|
+
this.userClaims$ = await this.authService.getUserClaims();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
126
178
|
|
|
127
|
-
|
|
179
|
+
### Token Refresh and Error Handling
|
|
128
180
|
|
|
129
181
|
```typescript
|
|
130
|
-
import {
|
|
182
|
+
import { Injectable } from '@angular/core';
|
|
183
|
+
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|
131
184
|
import { MJAuthBase } from '@memberjunction/ng-auth-services';
|
|
185
|
+
import { catchError, switchMap } from 'rxjs/operators';
|
|
186
|
+
import { throwError } from 'rxjs';
|
|
132
187
|
|
|
188
|
+
@Injectable({
|
|
189
|
+
providedIn: 'root'
|
|
190
|
+
})
|
|
133
191
|
export class ApiService {
|
|
134
|
-
constructor(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
192
|
+
constructor(
|
|
193
|
+
private http: HttpClient,
|
|
194
|
+
private authService: MJAuthBase
|
|
195
|
+
) {}
|
|
196
|
+
|
|
197
|
+
getData() {
|
|
198
|
+
return this.http.get('/api/data').pipe(
|
|
199
|
+
catchError((error: HttpErrorResponse) => this.handleError(error))
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async handleError(error: HttpErrorResponse) {
|
|
204
|
+
if (error.status === 401 && this.authService.checkExpiredTokenError(error.message)) {
|
|
205
|
+
// Token expired, try to refresh
|
|
206
|
+
const tokenObs = await this.authService.refresh();
|
|
207
|
+
return tokenObs.pipe(
|
|
208
|
+
switchMap(() => this.http.get('/api/data')) // Retry the request
|
|
209
|
+
);
|
|
146
210
|
}
|
|
211
|
+
return throwError(() => error);
|
|
147
212
|
}
|
|
148
213
|
}
|
|
149
214
|
```
|
|
150
215
|
|
|
151
|
-
|
|
216
|
+
### Protected Routes with Guards
|
|
152
217
|
|
|
153
218
|
```typescript
|
|
219
|
+
import { Injectable } from '@angular/core';
|
|
220
|
+
import { CanActivate, Router } from '@angular/router';
|
|
154
221
|
import { MJAuthBase } from '@memberjunction/ng-auth-services';
|
|
222
|
+
import { map, tap } from 'rxjs/operators';
|
|
155
223
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
224
|
+
@Injectable({
|
|
225
|
+
providedIn: 'root'
|
|
226
|
+
})
|
|
227
|
+
export class AuthGuard implements CanActivate {
|
|
228
|
+
constructor(
|
|
229
|
+
private authService: MJAuthBase,
|
|
230
|
+
private router: Router
|
|
231
|
+
) {}
|
|
232
|
+
|
|
233
|
+
async canActivate() {
|
|
234
|
+
const isAuthenticated$ = await this.authService.isAuthenticated();
|
|
235
|
+
return isAuthenticated$.pipe(
|
|
236
|
+
tap(authenticated => {
|
|
237
|
+
if (!authenticated) {
|
|
238
|
+
this.authService.login();
|
|
239
|
+
}
|
|
240
|
+
})
|
|
163
241
|
);
|
|
164
242
|
}
|
|
165
243
|
}
|
|
@@ -169,56 +247,132 @@ export class UserService {
|
|
|
169
247
|
|
|
170
248
|
### MJAuthBase (Abstract Class)
|
|
171
249
|
|
|
250
|
+
The base authentication service that all providers implement.
|
|
251
|
+
|
|
172
252
|
#### Properties
|
|
173
253
|
|
|
174
|
-
|
|
|
175
|
-
|
|
254
|
+
| Property | Type | Description |
|
|
255
|
+
|----------|------|-------------|
|
|
176
256
|
| `authenticated` | `boolean` | Current authentication state |
|
|
177
257
|
|
|
178
258
|
#### Methods
|
|
179
259
|
|
|
180
|
-
|
|
|
181
|
-
|
|
182
|
-
| `login` | `options?: any` | `Promise<any>` | Initiates the login
|
|
183
|
-
| `logout` | None | `Promise<any>` | Logs the user out |
|
|
260
|
+
| Method | Parameters | Return Type | Description |
|
|
261
|
+
|--------|------------|-------------|-------------|
|
|
262
|
+
| `login` | `options?: any` | `Promise<any>` | Initiates the login flow. Options vary by provider. |
|
|
263
|
+
| `logout` | None | `Promise<any>` | Logs the user out and clears authentication state |
|
|
184
264
|
| `refresh` | None | `Promise<Observable<any>>` | Refreshes the authentication token |
|
|
185
|
-
| `isAuthenticated` | None | `Promise<any>` |
|
|
186
|
-
| `getUser` | None | `Promise<any>` |
|
|
187
|
-
| `getUserClaims` | None | `Promise<Observable<any>>` |
|
|
188
|
-
| `checkExpiredTokenError` | `error: string` | `boolean` | Checks if an error
|
|
265
|
+
| `isAuthenticated` | None | `Promise<any>` | Returns an Observable of the authentication state |
|
|
266
|
+
| `getUser` | None | `Promise<any>` | Returns user information (format varies by provider) |
|
|
267
|
+
| `getUserClaims` | None | `Promise<Observable<any>>` | Returns the user's token claims |
|
|
268
|
+
| `checkExpiredTokenError` | `error: string` | `boolean` | Checks if an error indicates an expired token |
|
|
189
269
|
|
|
190
270
|
### MJAuth0Provider
|
|
191
271
|
|
|
192
|
-
Auth0-specific implementation of MJAuthBase
|
|
272
|
+
Auth0-specific implementation of `MJAuthBase`. Internally uses `@auth0/auth0-angular`.
|
|
273
|
+
|
|
274
|
+
#### Provider-Specific Behavior
|
|
275
|
+
- Uses Auth0's redirect flow for authentication
|
|
276
|
+
- Returns Auth0 `User` object from `getUser()`
|
|
277
|
+
- Token expiration check looks for "jwt expired" in error messages
|
|
193
278
|
|
|
194
279
|
### MJMSALProvider
|
|
195
280
|
|
|
196
|
-
MSAL-specific implementation of MJAuthBase
|
|
281
|
+
MSAL-specific implementation of `MJAuthBase`. Internally uses `@azure/msal-angular`.
|
|
282
|
+
|
|
283
|
+
#### Provider-Specific Behavior
|
|
284
|
+
- Implements initialization handling to ensure MSAL is ready
|
|
285
|
+
- Uses redirect flow with automatic account selection
|
|
286
|
+
- Returns MSAL `AccountInfo` from `getUser()`
|
|
287
|
+
- Includes refresh token policy in token operations
|
|
288
|
+
- Token expiration check looks for authorization errors
|
|
197
289
|
|
|
198
290
|
### AuthServicesModule
|
|
199
291
|
|
|
292
|
+
The main module for configuring authentication services.
|
|
293
|
+
|
|
200
294
|
#### Static Methods
|
|
201
295
|
|
|
202
|
-
|
|
|
203
|
-
|
|
204
|
-
| `forRoot` | `environment: AuthEnvironment` | `ModuleWithProviders<AuthServicesModule>` | Configures the
|
|
296
|
+
| Method | Parameters | Return Type | Description |
|
|
297
|
+
|--------|------------|-------------|-------------|
|
|
298
|
+
| `forRoot` | `environment: AuthEnvironment` | `ModuleWithProviders<AuthServicesModule>` | Configures the authentication module with environment settings |
|
|
205
299
|
|
|
206
300
|
#### AuthEnvironment Type
|
|
207
301
|
|
|
208
302
|
```typescript
|
|
209
303
|
type AuthEnvironment = {
|
|
210
|
-
AUTH_TYPE: string;
|
|
211
|
-
CLIENT_ID: string;
|
|
212
|
-
CLIENT_AUTHORITY: string;
|
|
213
|
-
AUTH0_CLIENTID: string;
|
|
214
|
-
AUTH0_DOMAIN: string;
|
|
304
|
+
AUTH_TYPE: string; // 'auth0' or 'msal'
|
|
305
|
+
CLIENT_ID: string; // MSAL client ID
|
|
306
|
+
CLIENT_AUTHORITY: string; // MSAL authority URL
|
|
307
|
+
AUTH0_CLIENTID: string; // Auth0 client ID
|
|
308
|
+
AUTH0_DOMAIN: string; // Auth0 domain
|
|
215
309
|
};
|
|
216
310
|
```
|
|
217
311
|
|
|
218
|
-
|
|
312
|
+
### RedirectComponent
|
|
313
|
+
|
|
314
|
+
Re-exported from `@azure/msal-angular`. Required for MSAL redirect flow handling.
|
|
315
|
+
|
|
316
|
+
## Integration with Other MemberJunction Packages
|
|
317
|
+
|
|
318
|
+
This package integrates with:
|
|
319
|
+
- **@memberjunction/core**: Uses core logging utilities (`LogError`)
|
|
320
|
+
- **MemberJunction Explorer**: Provides authentication for all Explorer UI components
|
|
321
|
+
- **MemberJunction API**: Token management for API authentication
|
|
322
|
+
|
|
323
|
+
## Build and Development
|
|
324
|
+
|
|
325
|
+
### Building the Package
|
|
326
|
+
```bash
|
|
327
|
+
# From the package directory
|
|
328
|
+
npm run build
|
|
329
|
+
|
|
330
|
+
# From the repository root
|
|
331
|
+
npm run build -- --filter="@memberjunction/ng-auth-services"
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Development Notes
|
|
335
|
+
- The package uses Angular Package Format (APF)
|
|
336
|
+
- Compiled with Angular Compiler (`ngc`)
|
|
337
|
+
- No side effects - tree-shakeable
|
|
338
|
+
- Distributed files are in the `/dist` directory
|
|
339
|
+
|
|
340
|
+
## Migration Guide
|
|
341
|
+
|
|
342
|
+
### Switching Between Providers
|
|
343
|
+
|
|
344
|
+
To switch authentication providers:
|
|
345
|
+
|
|
346
|
+
1. Update your environment configuration:
|
|
347
|
+
```typescript
|
|
348
|
+
// From Auth0 to MSAL
|
|
349
|
+
AUTH_TYPE: 'msal', // was 'auth0'
|
|
350
|
+
CLIENT_ID: 'your-azure-client-id',
|
|
351
|
+
CLIENT_AUTHORITY: 'https://login.microsoftonline.com/your-tenant',
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
2. Add redirect route (if switching to MSAL):
|
|
355
|
+
```typescript
|
|
356
|
+
{ path: 'auth', component: RedirectComponent }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
3. No other code changes required - the `MJAuthBase` interface remains the same
|
|
360
|
+
|
|
361
|
+
## Troubleshooting
|
|
362
|
+
|
|
363
|
+
### Common Issues
|
|
364
|
+
|
|
365
|
+
1. **MSAL Initialization Errors**: Ensure the MSAL redirect component is properly configured in routes
|
|
366
|
+
2. **Token Refresh Failures**: Check that refresh token is enabled in your auth provider configuration
|
|
367
|
+
3. **CORS Issues**: Verify redirect URIs are properly configured in your auth provider dashboard
|
|
368
|
+
|
|
369
|
+
### Debug Tips
|
|
370
|
+
|
|
371
|
+
- Check browser console for authentication errors
|
|
372
|
+
- Verify localStorage contains auth tokens
|
|
373
|
+
- Use browser dev tools to inspect network requests for auth headers
|
|
374
|
+
- Enable verbose logging in your auth provider configuration
|
|
375
|
+
|
|
376
|
+
## License
|
|
219
377
|
|
|
220
|
-
|
|
221
|
-
- @angular/core
|
|
222
|
-
- @memberjunction/core
|
|
223
|
-
- @auth0/auth0-angular (when using Auth0)
|
|
224
|
-
- @azure/msal-angular (when using MSAL)
|
|
378
|
+
ISC
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-auth-services",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.44.0",
|
|
4
4
|
"description": "MemberJunction Explorer: Authentication Services",
|
|
5
5
|
"main": "./dist/public-api.js",
|
|
6
6
|
"typings": "./dist/public-api.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@azure/msal-angular": "^3.0.11"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@memberjunction/core": "2.
|
|
30
|
+
"@memberjunction/core": "2.44.0",
|
|
31
31
|
"tslib": "^2.3.0"
|
|
32
32
|
},
|
|
33
33
|
"sideEffects": false
|