diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/data/user.ts')
-rw-r--r-- | Timeline/ClientApp/src/app/data/user.ts | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/Timeline/ClientApp/src/app/data/user.ts b/Timeline/ClientApp/src/app/data/user.ts new file mode 100644 index 00000000..8f787478 --- /dev/null +++ b/Timeline/ClientApp/src/app/data/user.ts @@ -0,0 +1,224 @@ +import axios, { AxiosError } from 'axios'; +import { useState, useEffect } from 'react'; +import { BehaviorSubject, Observable } from 'rxjs'; + +import { apiBaseUrl } from '../config'; +import { extractErrorCode } from './common'; +import { pushAlert } from '../common/alert-service'; +import { i18nPromise } from '../i18n'; +import { UiLogicError } from '../common'; + +export interface UserAuthInfo { + username: string; + administrator: boolean; +} + +export interface User { + username: string; + administrator: boolean; + nickname: string; + _links: { + avatar: string; + timeline: string; + }; +} + +export interface UserWithToken extends User { + token: string; +} + +interface CreateTokenRequest { + username: string; + password: string; +} + +interface CreateTokenResponse { + token: string; + user: User; +} + +interface VerifyTokenRequest { + token: string; +} + +interface VerifyTokenResponse { + user: User; +} + +export type LoginCredentials = CreateTokenRequest; + +const userSubject = new BehaviorSubject<UserWithToken | null | undefined>( + undefined +); + +export const user$: Observable<UserWithToken | null | undefined> = userSubject; + +export function getCurrentUser(): UserWithToken | null | undefined { + return userSubject.value; +} + +const kCreateTokenUrl = '/token/create'; +const kVerifyTokenUrl = '/token/verify'; +const createTokenUrl = apiBaseUrl + kCreateTokenUrl; +const verifyTokenUrl = apiBaseUrl + kVerifyTokenUrl; + +function verifyToken(token: string): Promise<User> { + return axios + .post<VerifyTokenResponse>(verifyTokenUrl, { + token: token, + } as VerifyTokenRequest) + .then((res) => res.data.user); +} + +const TOKEN_STORAGE_KEY = 'token'; + +export function checkUserLoginState(): Promise<UserWithToken | null> { + if (getCurrentUser() !== undefined) + throw new UiLogicError("Already checked user. Can't check twice."); + + const savedToken = window.localStorage.getItem(TOKEN_STORAGE_KEY); + if (savedToken) { + return verifyToken(savedToken) + .then( + (u) => { + const user: UserWithToken = { + ...u, + token: savedToken, + }; + void i18nPromise.then((t) => { + pushAlert({ + type: 'success', + message: t('user.welcomeBack'), + }); + }); + return user; + }, + (e: AxiosError) => { + if (e.response != null) { + window.localStorage.removeItem(TOKEN_STORAGE_KEY); + void i18nPromise.then((t) => { + pushAlert({ + type: 'danger', + message: t('user.verifyTokenFailed'), + }); + }); + } else { + void i18nPromise.then((t) => { + pushAlert({ + type: 'danger', + message: t('user.verifyTokenFailedNetwork'), + }); + }); + } + + return null; + } + ) + .then((u) => { + userSubject.next(u); + return u; + }); + } + userSubject.next(null); + return Promise.resolve(null); +} + +export class BadCredentialError { + constructor(public innerError: Error) {} + + message = 'login.badCredential'; +} + +export function userLogin( + credentials: LoginCredentials, + rememberMe: boolean +): Promise<UserWithToken> { + if (getCurrentUser()) { + throw new UiLogicError('Already login.'); + } + return axios + .post<CreateTokenResponse>(createTokenUrl, { ...credentials, expire: 30 }) + .catch((e: AxiosError) => { + if (extractErrorCode(e) === 11010101) { + throw new BadCredentialError(e); + } + throw e; + }) + .then((res) => { + const body = res.data; + const token = body.token; + if (rememberMe) { + window.localStorage.setItem(TOKEN_STORAGE_KEY, token); + } + const user = { + ...body.user, + token, + }; + userSubject.next(user); + return user; + }); +} + +export function userLogout(): void { + if (getCurrentUser() === undefined) { + throw new UiLogicError('Please check user first.'); + } + if (getCurrentUser() === null) { + throw new UiLogicError('No login.'); + } + window.localStorage.removeItem(TOKEN_STORAGE_KEY); + userSubject.next(null); +} + +export function useOptionalUser(): UserWithToken | null | undefined { + const [user, setUser] = useState<UserWithToken | null | undefined>( + userSubject.value + ); + useEffect(() => { + const sub = user$.subscribe((u) => setUser(u)); + return () => { + sub.unsubscribe(); + }; + }); + return user; +} + +export function useUser(): UserWithToken | null { + const [user, setUser] = useState<UserWithToken | null>(() => { + const initUser = userSubject.value; + if (initUser === undefined) { + throw new UiLogicError( + "This is a logic error in user module. Current user can't be undefined in useUser." + ); + } + return initUser; + }); + useEffect(() => { + const sub = user$.subscribe((u) => { + if (u === undefined) { + throw new UiLogicError( + "This is a logic error in user module. User emitted can't be undefined later." + ); + } + setUser(u); + }); + return () => { + sub.unsubscribe(); + }; + }); + return user; +} + +export function useUserLoggedIn(): UserWithToken { + const user = useUser(); + if (user == null) { + throw new UiLogicError('You assert user has logged in but actually not.'); + } + return user; +} + +export function fetchUser(username: string): Promise<User> { + return axios + .get<User>(`${apiBaseUrl}/users/${username}`) + .then((res) => res.data); +} |