aboutsummaryrefslogtreecommitdiff
path: root/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts
blob: 66eafde9cc3306b12fbb03fac580c6e5fa01c052 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import { Injectable, Inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';

import { Observable, throwError, BehaviorSubject, of } from 'rxjs';
import { map, catchError, retry, switchMap, tap, filter } from 'rxjs/operators';

import { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError, ServerInternalError } from './errors';
import {
  createTokenUrl, validateTokenUrl, CreateTokenRequest,
  CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse
} from './http-entities';
import { UserCredentials, UserInfo } from '../entities';
import { MatSnackBar } from '@angular/material';
import { WINDOW } from '../window-inject-token';

export const snackBarText = {
  checkFail: 'Failed to check last login',
  noLogin: 'No login before!',
  alreadyLogin: 'You have login already!',
  invalidLogin: 'Last login is no longer invalid!',
  ok: 'ok'
};

export type SnackBarTextKey = Exclude<keyof typeof snackBarText, 'ok'>;

export const TOKEN_STORAGE_KEY = 'token';

export interface LoginInfo extends UserCredentials {
  rememberMe: boolean;
}

/**
 * This service is only used internal in user module.
 */
@Injectable({
  providedIn: 'root'
})
export class InternalUserService {

  private token: string | null = null;
  private userInfoSubject = new BehaviorSubject<UserInfo | null | undefined>(undefined);

  readonly userInfo$: Observable<UserInfo | null> =
    <Observable<UserInfo | null>>this.userInfoSubject.pipe(filter(value => value !== undefined));

  get currentUserInfo(): UserInfo | null | undefined {
    return this.userInfoSubject.value;
  }

  private openSnackBar(snackBar: MatSnackBar, textKey: SnackBarTextKey) {
    setTimeout(() => snackBar.open(snackBarText[textKey], snackBarText.ok, { duration: 2000 }), 0);
  }

  constructor(@Inject(WINDOW) private window: Window, private httpClient: HttpClient, private router: Router, snackBar: MatSnackBar) {
    const savedToken = this.window.localStorage.getItem(TOKEN_STORAGE_KEY);
    if (savedToken === null) {
      this.openSnackBar(snackBar, 'noLogin');
      this.userInfoSubject.next(null);
    } else {
      this.validateToken(savedToken).subscribe(result => {
        if (result === null) {
          this.window.localStorage.removeItem(TOKEN_STORAGE_KEY);
          this.openSnackBar(snackBar, 'invalidLogin');
          this.userInfoSubject.next(null);
        } else {
          this.token = savedToken;
          this.userInfoSubject.next(result);
          this.openSnackBar(snackBar, 'alreadyLogin');
        }
      }, _ => {
        this.openSnackBar(snackBar, 'checkFail');
        this.userInfoSubject.next(null);
      });
    }
  }

  private validateToken(token: string): Observable<UserInfo | null> {
    return this.httpClient.post<ValidateTokenResponse>(validateTokenUrl, <ValidateTokenRequest>{ token: token }).pipe(
      retry(3),
      switchMap(result => {
        if (result.isValid) {
          const { userInfo } = result;
          if (userInfo) {
            return of(userInfo);
          } else {
            return throwError(new ServerInternalError('IsValid is true but UserInfo is null.'));
          }
        } else {
          return of(null);
        }
      }),
      tap({
        error: error => {
          console.error('Failed to validate token.');
          console.error(error);
        }
      }),
    );
  }

  userRouteNavigate(commands: any[] | null) {
    this.router.navigate([{
      outlets: {
        user: commands
      }
    }]);
  }

  tryLogin(info: LoginInfo): Observable<UserInfo> {
    if (this.token) {
      return throwError(new AlreadyLoginError());
    }

    return this.httpClient.post<CreateTokenResponse>(createTokenUrl, <CreateTokenRequest>info).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.error instanceof ErrorEvent) {
          console.error('An error occurred when login: ' + error.error.message);
          return throwError(new BadNetworkError());
        } else {
          console.error('An unknown error occurred when login: ' + error);
          return throwError(new UnknownError(error));
        }
      }),
      switchMap(result => {
        if (result.success) {
          if (result.token && result.userInfo) {
            this.token = result.token;
            if (info.rememberMe) {
              this.window.localStorage.setItem(TOKEN_STORAGE_KEY, result.token);
            }
            this.userInfoSubject.next(result.userInfo);
            return of(result.userInfo);
          } else {
          console.error('An error occurred when login: server return wrong data.');
          return throwError(new ServerInternalError('Token or userInfo is null.'));
          }
        } else {
          console.error('An error occurred when login: wrong credentials.');
          return throwError(new BadCredentialsError());
        }
      })
    );
  }

  logout() {
    if (this.currentUserInfo === null) {
      throw new Error('No login now. You can\'t logout.');
    }

    this.window.localStorage.removeItem(TOKEN_STORAGE_KEY);
    this.token = null;
    this.userInfoSubject.next(null);
  }
}