diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/services/user.ts')
-rw-r--r-- | Timeline/ClientApp/src/app/services/user.ts | 393 |
1 files changed, 0 insertions, 393 deletions
diff --git a/Timeline/ClientApp/src/app/services/user.ts b/Timeline/ClientApp/src/app/services/user.ts deleted file mode 100644 index f253fc19..00000000 --- a/Timeline/ClientApp/src/app/services/user.ts +++ /dev/null @@ -1,393 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { BehaviorSubject, Observable, from } from "rxjs"; -import { map, filter } from "rxjs/operators"; - -import { UiLogicError } from "@/common"; -import { convertError } from "@/utilities/rxjs"; - -import { HttpNetworkError, BlobWithEtag, NotModified } from "@/http/common"; -import { - getHttpTokenClient, - HttpCreateTokenBadCredentialError, -} from "@/http/token"; -import { - getHttpUserClient, - HttpUserNotExistError, - HttpUser, -} from "@/http/user"; - -import { dataStorage, throwIfNotNetworkError } from "./common"; -import { DataHub } from "./DataHub"; -import { pushAlert } from "./alert"; - -export type User = HttpUser; - -export interface UserAuthInfo { - username: string; - administrator: boolean; -} - -export interface UserWithToken extends User { - token: string; -} - -export interface LoginCredentials { - username: string; - password: string; -} - -export class BadCredentialError { - message = "login.badCredential"; -} - -const USER_STORAGE_KEY = "currentuser"; - -export class UserService { - private userSubject = new BehaviorSubject<UserWithToken | null | undefined>( - undefined - ); - - get user$(): Observable<UserWithToken | null | undefined> { - return this.userSubject; - } - - get currentUser(): UserWithToken | null | undefined { - return this.userSubject.value; - } - - async checkLoginState(): Promise<UserWithToken | null> { - if (this.currentUser !== undefined) { - console.warn("Already checked user. Can't check twice."); - } - - const savedUser = await dataStorage.getItem<UserWithToken | null>( - USER_STORAGE_KEY - ); - - if (savedUser == null) { - this.userSubject.next(null); - return null; - } - - this.userSubject.next(savedUser); - - const savedToken = savedUser.token; - try { - const res = await getHttpTokenClient().verify({ token: savedToken }); - const user: UserWithToken = { ...res.user, token: savedToken }; - await dataStorage.setItem<UserWithToken>(USER_STORAGE_KEY, user); - this.userSubject.next(user); - pushAlert({ - type: "success", - message: { - type: "i18n", - key: "user.welcomeBack", - }, - }); - return user; - } catch (error) { - if (error instanceof HttpNetworkError) { - pushAlert({ - type: "danger", - message: { type: "i18n", key: "user.verifyTokenFailedNetwork" }, - }); - return savedUser; - } else { - await dataStorage.removeItem(USER_STORAGE_KEY); - this.userSubject.next(null); - pushAlert({ - type: "danger", - message: { type: "i18n", key: "user.verifyTokenFailed" }, - }); - return null; - } - } - } - - async login( - credentials: LoginCredentials, - rememberMe: boolean - ): Promise<void> { - if (this.currentUser) { - throw new UiLogicError("Already login."); - } - try { - const res = await getHttpTokenClient().create({ - ...credentials, - expire: 30, - }); - const user: UserWithToken = { - ...res.user, - token: res.token, - }; - if (rememberMe) { - await dataStorage.setItem<UserWithToken>(USER_STORAGE_KEY, user); - } - this.userSubject.next(user); - } catch (e) { - if (e instanceof HttpCreateTokenBadCredentialError) { - throw new BadCredentialError(); - } else { - throw e; - } - } - } - - async logout(): Promise<void> { - if (this.currentUser === undefined) { - throw new UiLogicError("Please check user first."); - } - if (this.currentUser === null) { - throw new UiLogicError("No login."); - } - await dataStorage.removeItem(USER_STORAGE_KEY); - this.userSubject.next(null); - } - - changePassword( - oldPassword: string, - newPassword: string - ): Observable<unknown> { - if (this.currentUser == undefined) { - throw new UiLogicError("Not login or checked now, can't log out."); - } - const $ = from( - getHttpUserClient().changePassword( - { - oldPassword, - newPassword, - }, - this.currentUser.token - ) - ); - $.subscribe(() => { - void this.logout(); - }); - return $; - } -} - -export const userService = new UserService(); - -export function useRawUser(): UserWithToken | null | undefined { - const [user, setUser] = useState<UserWithToken | null | undefined>( - userService.currentUser - ); - useEffect(() => { - const subscription = userService.user$.subscribe((u) => setUser(u)); - return () => { - subscription.unsubscribe(); - }; - }); - return user; -} - -export function useUser(): UserWithToken | null { - const [user, setUser] = useState<UserWithToken | null>(() => { - const initUser = userService.currentUser; - 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 = userService.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 checkLogin(): UserWithToken { - const user = userService.currentUser; - if (user == null) { - throw new UiLogicError("You must login to perform the operation."); - } - return user; -} - -export class UserNotExistError extends Error {} - -export class UserInfoService { - saveUser(user: HttpUser): void { - const key = user.username; - void this._userHub.optionalInitLineWithSyncAction(key, async (line) => { - await this.doSaveUser(user); - line.next({ user, type: "synced" }); - }); - } - - saveUsers(users: HttpUser[]): void { - return users.forEach((user) => this.saveUser(user)); - } - - private getCachedUser(username: string): Promise<User | null> { - return dataStorage.getItem<HttpUser | null>(`user.${username}`); - } - - private doSaveUser(user: HttpUser): Promise<void> { - return dataStorage.setItem<HttpUser>(`user.${user.username}`, user).then(); - } - - syncUser(username: string): Promise<void> { - return this._userHub.getLineOrCreate(username).sync(); - } - - private _userHub = new DataHub< - string, - | { user: User; type: "cache" | "synced" | "offline" } - | { user?: undefined; type: "notexist" | "offline" } - >({ - sync: async (key, line) => { - if (line.value == undefined) { - const cache = await this.getCachedUser(key); - if (cache != null) { - line.next({ user: cache, type: "cache" }); - } - } - - try { - const res = await getHttpUserClient().get(key); - await this.doSaveUser(res); - line.next({ user: res, type: "synced" }); - } catch (e) { - if (e instanceof HttpUserNotExistError) { - line.next({ type: "notexist" }); - } else { - const cache = await this.getCachedUser(key); - line.next({ user: cache ?? undefined, type: "offline" }); - throwIfNotNetworkError(e); - } - } - }, - }); - - getUser$(username: string): Observable<User> { - return this._userHub.getObservable(username).pipe( - map((state) => state?.user), - filter((user): user is User => user != null) - ); - } - - private getCachedAvatar(username: string): Promise<BlobWithEtag | null> { - return dataStorage.getItem<BlobWithEtag | null>(`user.${username}.avatar`); - } - - private saveAvatar(username: string, data: BlobWithEtag): Promise<void> { - return dataStorage - .setItem<BlobWithEtag>(`user.${username}.avatar`, data) - .then(); - } - - syncAvatar(username: string): Promise<void> { - return this._avatarHub.getLineOrCreate(username).sync(); - } - - private _avatarHub = new DataHub< - string, - | { data: Blob; type: "cache" | "synced" | "offline" } - | { data?: undefined; type: "notexist" | "offline" } - >({ - sync: async (key, line) => { - const cache = await this.getCachedAvatar(key); - if (line.value == null) { - if (cache != null) { - line.next({ data: cache.data, type: "cache" }); - } - } - - if (cache == null) { - try { - const avatar = await getHttpUserClient().getAvatar(key); - await this.saveAvatar(key, avatar); - line.next({ data: avatar.data, type: "synced" }); - } catch (e) { - line.next({ type: "offline" }); - throwIfNotNetworkError(e); - } - } else { - try { - const res = await getHttpUserClient().getAvatar(key, cache.etag); - if (res instanceof NotModified) { - line.next({ data: cache.data, type: "synced" }); - } else { - const avatar = res; - await this.saveAvatar(key, avatar); - line.next({ data: avatar.data, type: "synced" }); - } - } catch (e) { - line.next({ data: cache.data, type: "offline" }); - throwIfNotNetworkError(e); - } - } - }, - }); - - getAvatar$(username: string): Observable<Blob> { - return this._avatarHub.getObservable(username).pipe( - map((state) => state.data), - filter((blob): blob is Blob => blob != null) - ); - } - - getUserInfo(username: string): Observable<User> { - return from(getHttpUserClient().get(username)).pipe( - convertError(HttpUserNotExistError, UserNotExistError) - ); - } - - async setAvatar(username: string, blob: Blob): Promise<void> { - const user = checkLogin(); - await getHttpUserClient().putAvatar(username, blob, user.token); - this._avatarHub.getLine(username)?.next({ data: blob, type: "synced" }); - } - - async setNickname(username: string, nickname: string): Promise<void> { - const user = checkLogin(); - return getHttpUserClient() - .patch(username, { nickname }, user.token) - .then((user) => { - this.saveUser(user); - }); - } -} - -export const userInfoService = new UserInfoService(); - -export function useAvatar(username?: string): Blob | undefined { - const [state, setState] = React.useState<Blob | undefined>(undefined); - React.useEffect(() => { - if (username == null) { - setState(undefined); - return; - } - - const subscription = userInfoService - .getAvatar$(username) - .subscribe((blob) => { - setState(blob); - }); - return () => { - subscription.unsubscribe(); - }; - }, [username]); - return state; -} |