aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/services/user.ts
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/services/user.ts')
-rw-r--r--FrontEnd/src/services/user.ts228
1 files changed, 228 insertions, 0 deletions
diff --git a/FrontEnd/src/services/user.ts b/FrontEnd/src/services/user.ts
new file mode 100644
index 00000000..3375c88a
--- /dev/null
+++ b/FrontEnd/src/services/user.ts
@@ -0,0 +1,228 @@
+import { useState, useEffect } from "react";
+import { BehaviorSubject, Observable } from "rxjs";
+
+import { UiLogicError } from "@/common";
+
+import { HttpNetworkError, setHttpToken } from "http/common";
+import {
+ getHttpTokenClient,
+ HttpCreateTokenBadCredentialError,
+} from "http/token";
+import { getHttpUserClient, HttpUser, UserPermission } from "http/user";
+
+import { pushAlert } from "./alert";
+
+interface IAuthUser extends HttpUser {
+ token: string;
+}
+
+export class AuthUser implements IAuthUser {
+ constructor(user: HttpUser, public token: string) {
+ this.uniqueId = user.uniqueId;
+ this.username = user.username;
+ this.permissions = user.permissions;
+ this.nickname = user.nickname;
+ }
+
+ uniqueId: string;
+ username: string;
+ permissions: UserPermission[];
+ nickname: string;
+
+ get hasAdministrationPermission(): boolean {
+ return this.permissions.length !== 0;
+ }
+
+ get hasAllTimelineAdministrationPermission(): boolean {
+ return this.permissions.includes("AllTimelineManagement");
+ }
+
+ get hasHighlightTimelineAdministrationPermission(): boolean {
+ return this.permissions.includes("HighlightTimelineManagement");
+ }
+}
+
+export interface LoginCredentials {
+ username: string;
+ password: string;
+}
+
+export class BadCredentialError {
+ message = "login.badCredential";
+}
+
+const USER_STORAGE_KEY = "currentuser";
+
+export class UserService {
+ constructor() {
+ this.userSubject.subscribe((u) => {
+ setHttpToken(u?.token ?? null);
+ });
+ }
+
+ private userSubject = new BehaviorSubject<AuthUser | null | undefined>(
+ undefined
+ );
+
+ get user$(): Observable<AuthUser | null | undefined> {
+ return this.userSubject;
+ }
+
+ get currentUser(): AuthUser | null | undefined {
+ return this.userSubject.value;
+ }
+
+ async checkLoginState(): Promise<AuthUser | null> {
+ if (this.currentUser !== undefined) {
+ console.warn("Already checked user. Can't check twice.");
+ }
+
+ const savedUserString = localStorage.getItem(USER_STORAGE_KEY);
+
+ const savedAuthUserData =
+ savedUserString == null
+ ? null
+ : (JSON.parse(savedUserString) as IAuthUser);
+
+ const savedUser =
+ savedAuthUserData == null
+ ? null
+ : new AuthUser(savedAuthUserData, savedAuthUserData.token);
+
+ 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 = new AuthUser(res.user, savedToken);
+ localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user));
+ this.userSubject.next(user);
+ pushAlert({
+ type: "success",
+ message: "user.welcomeBack",
+ });
+ return user;
+ } catch (error) {
+ if (error instanceof HttpNetworkError) {
+ pushAlert({
+ type: "danger",
+ message: "user.verifyTokenFailedNetwork",
+ });
+ return savedUser;
+ } else {
+ localStorage.removeItem(USER_STORAGE_KEY);
+ this.userSubject.next(null);
+ pushAlert({
+ type: "danger",
+ message: "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 = new AuthUser(res.user, res.token);
+ if (rememberMe) {
+ localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user));
+ }
+ this.userSubject.next(user);
+ } catch (e) {
+ if (e instanceof HttpCreateTokenBadCredentialError) {
+ throw new BadCredentialError();
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ logout(): Promise<void> {
+ if (this.currentUser === undefined) {
+ throw new UiLogicError("Please check user first.");
+ }
+ if (this.currentUser === null) {
+ throw new UiLogicError("No login.");
+ }
+ localStorage.removeItem(USER_STORAGE_KEY);
+ this.userSubject.next(null);
+ return Promise.resolve();
+ }
+
+ changePassword(oldPassword: string, newPassword: string): Promise<void> {
+ if (this.currentUser == undefined) {
+ throw new UiLogicError("Not login or checked now, can't log out.");
+ }
+
+ return getHttpUserClient()
+ .changePassword({
+ oldPassword,
+ newPassword,
+ })
+ .then(() => this.logout());
+ }
+}
+
+export const userService = new UserService();
+
+export function useRawUser(): AuthUser | null | undefined {
+ const [user, setUser] = useState<AuthUser | null | undefined>(
+ userService.currentUser
+ );
+ useEffect(() => {
+ const subscription = userService.user$.subscribe((u) => setUser(u));
+ return () => {
+ subscription.unsubscribe();
+ };
+ });
+ return user;
+}
+
+export function useUser(): AuthUser | null {
+ const [user, setUser] = useState<AuthUser | 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(): AuthUser {
+ const user = useUser();
+ if (user == null) {
+ throw new UiLogicError("You assert user has logged in but actually not.");
+ }
+ return user;
+}