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( 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 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 { if (getCurrentUser()) { throw new UiLogicError('Already login.'); } return axios .post(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( 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 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 { return axios .get(`${apiBaseUrl}/users/${username}`) .then((res) => res.data); }