Kaynağa Gözat

feat(admin-ui): Redirect to last route on log in after session expires

Relates to #19
Michael Bromley 7 yıl önce
ebeveyn
işleme
9a58320cf1

+ 0 - 2
admin-ui/src/app/administrator/components/admin-detail/admin-detail.component.ts

@@ -85,7 +85,6 @@ export class AdminDetailComponent extends BaseDetailComponent<Administrator> imp
             err => {
                 this.notificationService.error(_('common.notify-create-error'), {
                     entity: 'Administrator',
-                    error: err.message,
                 });
             },
         );
@@ -119,7 +118,6 @@ export class AdminDetailComponent extends BaseDetailComponent<Administrator> imp
                 err => {
                     this.notificationService.error(_('common.notify-update-error'), {
                         entity: 'Administrator',
-                        error: err.message,
                     });
                 },
             );

+ 0 - 2
admin-ui/src/app/administrator/components/role-detail/role-detail.component.ts

@@ -82,7 +82,6 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
             err => {
                 this.notificationService.error(_('common.notify-create-error'), {
                     entity: 'Role',
-                    error: err.message,
                 });
             },
         );
@@ -113,7 +112,6 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
                 err => {
                     this.notificationService.error(_('common.notify-update-error'), {
                         entity: 'Role',
-                        error: err.message,
                     });
                 },
             );

+ 0 - 2
admin-ui/src/app/catalog/components/facet-detail/facet-detail.component.ts

@@ -128,7 +128,6 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues> i
                 err => {
                     this.notificationService.error(_('common.notify-create-error'), {
                         entity: 'Facet',
-                        error: err.message,
                     });
                 },
             );
@@ -181,7 +180,6 @@ export class FacetDetailComponent extends BaseDetailComponent<FacetWithValues> i
                 },
                 err => {
                     this.notificationService.error(_('common.notify-update-error'), {
-                        error: err.message,
                         entity: 'Facet',
                     });
                 },

+ 0 - 2
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -151,7 +151,6 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                 err => {
                     this.notificationService.error(_('common.notify-create-error'), {
                         entity: 'Product',
-                        error: err.message,
                     });
                 },
             );
@@ -198,7 +197,6 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                 },
                 err => {
                     this.notificationService.error(_('common.notify-update-error'), {
-                        error: err.message,
                         entity: 'Product',
                     });
                 },

+ 48 - 15
admin-ui/src/app/data/providers/interceptor.ts

@@ -7,20 +7,31 @@ import {
     HttpResponse,
 } from '@angular/common/http';
 import { Injectable, Injector } from '@angular/core';
+import { Router } from '@angular/router';
 import { Observable } from 'rxjs';
 import { tap } from 'rxjs/operators';
 
+import { API_URL } from '../../app.config';
+import { AuthService } from '../../core/providers/auth/auth.service';
+import { _ } from '../../core/providers/i18n/mark-for-extraction';
 import { NotificationService } from '../../core/providers/notification/notification.service';
 
 import { DataService } from './data.service';
 
+export const AUTH_REDIRECT_PARAM = 'redirectTo';
+
 /**
  * The default interceptor examines all HTTP requests & responses and automatically updates the requesting state
  * and shows error notifications.
  */
 @Injectable()
 export class DefaultInterceptor implements HttpInterceptor {
-    constructor(private dataService: DataService, private injector: Injector) {}
+    constructor(
+        private dataService: DataService,
+        private injector: Injector,
+        private authService: AuthService,
+        private router: Router,
+    ) {}
 
     intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
         this.dataService.client.startRequest().subscribe();
@@ -28,13 +39,13 @@ export class DefaultInterceptor implements HttpInterceptor {
             tap(
                 event => {
                     if (event instanceof HttpResponse) {
-                        this.notifyOnGraphQLErrors(event);
+                        this.notifyOnError(event);
                         this.dataService.client.completeRequest().subscribe();
                     }
                 },
                 err => {
                     if (err instanceof HttpErrorResponse) {
-                        this.displayErrorNotification(err.message);
+                        this.notifyOnError(err);
                         this.dataService.client.completeRequest().subscribe();
                     }
                 },
@@ -42,15 +53,37 @@ export class DefaultInterceptor implements HttpInterceptor {
         );
     }
 
-    /**
-     * GraphQL errors still return 200 OK responses, but have the actual error message
-     * inside the body of the response.
-     */
-    private notifyOnGraphQLErrors(response: HttpResponse<any>): void {
-        const graqhQLErrors = response.body.errors;
-        if (graqhQLErrors && Array.isArray(graqhQLErrors)) {
-            const message = graqhQLErrors.map(err => err.message).join('\n');
-            this.displayErrorNotification(message);
+    private notifyOnError(response: HttpResponse<any> | HttpErrorResponse) {
+        if (response instanceof HttpErrorResponse) {
+            if (response.status === 0) {
+                this.displayErrorNotification(_(`error.could-not-connect-to-server`), { url: API_URL });
+            } else {
+                this.displayErrorNotification(response.toString());
+            }
+        } else {
+            // GraphQL errors still return 200 OK responses, but have the actual error message
+            // inside the body of the response.
+            const graqhQLErrors = response.body.errors;
+            if (graqhQLErrors && Array.isArray(graqhQLErrors)) {
+                const firstStatus: number = graqhQLErrors[0].message.statusCode;
+                switch (firstStatus) {
+                    case 401:
+                        this.displayErrorNotification(_(`error.401-unauthorized`));
+                        break;
+                    case 403:
+                        this.displayErrorNotification(_(`error.403-forbidden`));
+                        this.authService.logOut();
+                        this.router.navigate(['/login'], {
+                            queryParams: {
+                                [AUTH_REDIRECT_PARAM]: btoa(this.router.url),
+                            },
+                        });
+                        break;
+                    default:
+                        const message = graqhQLErrors.map(err => err.message.error).join('\n');
+                        this.displayErrorNotification(message);
+                }
+            }
         }
     }
 
@@ -59,8 +92,8 @@ export class DefaultInterceptor implements HttpInterceptor {
      * eventually depends on the HttpClient (used to load messages from json files). If we were to
      * directly inject NotificationService into the constructor, we get a cyclic dependency.
      */
-    private displayErrorNotification(message: string): void {
-        const notificationService = this.injector.get(NotificationService);
-        notificationService.error(message);
+    private displayErrorNotification(message: string, vars?: Record<string, any>): void {
+        const notificationService = this.injector.get<NotificationService>(NotificationService);
+        notificationService.error(message, vars);
     }
 }

+ 8 - 3
admin-ui/src/app/data/server-config.ts

@@ -30,9 +30,14 @@ export class ServerConfigService {
             baseDataService
                 .query<GetServerConfig>(GET_SERVER_CONFIG)
                 .single$.toPromise()
-                .then(result => {
-                    this._serverConfig = result.config;
-                });
+                .then(
+                    result => {
+                        this._serverConfig = result.config;
+                    },
+                    err => {
+                        // Let the error fall through to be caught by the http interceptor.
+                    },
+                );
     }
 
     get serverConfig(): ServerConfig {

+ 4 - 4
admin-ui/src/app/login/components/login/login.component.html

@@ -22,10 +22,10 @@
                     {{ 'common.remember-me' | translate }}
                 </label>
             </div>
-            <div class="error active" *ngIf="lastError">
-                {{ lastError }}
-            </div>
-            <button type="submit" class="btn btn-primary" (click)="logIn()">{{ 'common.login' | translate }}</button>
+            <button type="submit"
+                    class="btn btn-primary"
+                    (click)="logIn()"
+                    [disabled]="!username || !password">{{ 'common.login' | translate }}</button>
         </div>
     </form>
 </div>

+ 25 - 15
admin-ui/src/app/login/components/login/login.component.ts

@@ -1,9 +1,8 @@
-import { HttpErrorResponse } from '@angular/common/http';
 import { Component } from '@angular/core';
 import { Router } from '@angular/router';
 
-import { API_URL } from '../../../app.config';
 import { AuthService } from '../../../core/providers/auth/auth.service';
+import { AUTH_REDIRECT_PARAM } from '../../../data/providers/interceptor';
 
 @Component({
     selector: 'vdr-login',
@@ -13,25 +12,36 @@ import { AuthService } from '../../../core/providers/auth/auth.service';
 export class LoginComponent {
     username = '';
     password = '';
-    lastError = '';
 
     constructor(private authService: AuthService, private router: Router) {}
 
     logIn(): void {
         this.authService.logIn(this.username, this.password).subscribe(
-            () => this.router.navigate(['/']),
-            (err: HttpErrorResponse) => {
-                switch (err.status) {
-                    case 401:
-                        this.lastError = 'Invalid username or password';
-                        break;
-                    case 0:
-                        this.lastError = `Could not connect to the Vendure server at ${API_URL}`;
-                        break;
-                    default:
-                        this.lastError = err.message;
-                }
+            () => {
+                const redirect = this.getRedirectRoute();
+                this.router.navigate([redirect ? redirect : '/']);
+            },
+            err => {
+                /* error handled by http interceptor */
             },
         );
     }
+
+    /**
+     * Attemps to read a redirect param from the current url and parse it into a
+     * route from which the user was redirected after a 401 error.
+     */
+    private getRedirectRoute(): string | undefined {
+        let redirectTo: string | undefined;
+        const re = new RegExp(`${AUTH_REDIRECT_PARAM}=(.*)`);
+        try {
+            const redirectToParam = window.location.search.match(re);
+            if (redirectToParam && 1 < redirectToParam.length) {
+                redirectTo = atob(decodeURIComponent(redirectToParam[1]));
+            }
+        } catch (e) {
+            // ignore
+        }
+        return redirectTo;
+    }
 }

+ 5 - 2
admin-ui/src/i18n-messages/en.json

@@ -91,9 +91,9 @@
     "log-out": "Log out",
     "login": "Log in",
     "next": "Next",
-    "notify-create-error": "An error occurred, could not create { entity }\n\n { error }",
+    "notify-create-error": "An error occurred, could not create { entity }",
     "notify-create-success": "Created new { entity }",
-    "notify-update-error": "An error occurred, could not update { entity }\n\n { error }",
+    "notify-update-error": "An error occurred, could not update { entity }",
     "notify-update-success": "Updated { entity }",
     "password": "Password",
     "remember-me": "Remember me",
@@ -101,6 +101,9 @@
     "username": "Username"
   },
   "error": {
+    "401-unauthorized": "Invalid login. Please try again",
+    "403-forbidden": "Your session has expired. Please log in",
+    "could-not-connect-to-server": "Could not connect to the Vendure server at { url }",
     "facet-value-form-values-do-not-match": "The number of values in the facet form does not match the actual number of values",
     "product-variant-form-values-do-not-match": "The number of variants in the product form does not match the actual number of variants"
   },