From 68ca8b0976efe90c0c40bcae69f0825671b98bad Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 30 May 2020 16:23:25 +0800 Subject: Merge front end to this repo. But I need to wait for aspnet core support for custom port and package manager for dev server. --- Timeline/ClientApp/src/data/user.ts | 215 ++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 Timeline/ClientApp/src/data/user.ts (limited to 'Timeline/ClientApp/src/data/user.ts') diff --git a/Timeline/ClientApp/src/data/user.ts b/Timeline/ClientApp/src/data/user.ts new file mode 100644 index 00000000..755aecf6 --- /dev/null +++ b/Timeline/ClientApp/src/data/user.ts @@ -0,0 +1,215 @@ +import axios, { AxiosError } from 'axios'; +import { useState, useEffect } from 'react'; +import { BehaviorSubject, Observable } from 'rxjs'; + +import { apiBaseUrl } from '../config'; +import { pushAlert } from '../common/alert-service'; +import { i18nPromise } from '../i18n'; + +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( + undefined +); + +export const user$: Observable = 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 { + return axios + .post(verifyTokenUrl, { + token: token + } as VerifyTokenRequest) + .then(res => res.data.user); +} + +const TOKEN_STORAGE_KEY = 'token'; + +export function checkUserLoginState(): Promise { + if (getCurrentUser() !== undefined) + throw new Error("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 + }; + i18nPromise.then(t => { + pushAlert({ + type: 'success', + message: t('user.welcomeBack') + }); + }); + return user; + }, + (e: AxiosError) => { + if (e.response != null) { + window.localStorage.removeItem(TOKEN_STORAGE_KEY); + i18nPromise.then(t => { + pushAlert({ + type: 'danger', + message: t('user.verifyTokenFailed') + }); + }); + } else { + 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 { + if (getCurrentUser()) { + throw new Error('Already login.'); + } + return axios + .post(createTokenUrl, { ...credentials, expire: 30 }) + .catch(e => { + const error = e as AxiosError; + if (error.response?.data?.code === 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 Error('Please check user first.'); + } + if (getCurrentUser() === null) { + throw new Error('No login.'); + } + window.localStorage.removeItem(TOKEN_STORAGE_KEY); + userSubject.next(null); +} + +export function useOptionalUser(): UserWithToken | null | undefined { + const [user, setUser] = useState( + userSubject.value + ); + useEffect(() => { + const sub = user$.subscribe(u => setUser(u)); + return () => { + sub.unsubscribe(); + }; + }); + return user; +} + +export function useUser(): UserWithToken | null { + const [user, setUser] = useState(() => { + const initUser = userSubject.value; + if (initUser === undefined) { + throw new Error( + "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 Error( + "This is a logic error in user module. User emitted can't be undefined later." + ); + } + setUser(u); + }); + return () => { + sub.unsubscribe(); + }; + }); + return user; +} + +export function fetchUser(username: string): Promise { + return axios + .get(`${apiBaseUrl}/users/${username}`) + .then(res => res.data); +} -- cgit v1.2.3