From 700eaf3eb20a500f73f5bc426a7b15828b2341dd Mon Sep 17 00:00:00 2001 From: john Date: Tue, 20 May 2025 10:06:18 +0200 Subject: [PATCH] use user from session --- src/App.tsx | 40 ++++-------------- src/app/auth/authService.ts | 26 ++++-------- src/app/auth/components/AuthNavButtons.tsx | 4 +- src/app/auth/components/Protected.tsx | 4 +- src/app/auth/components/RefreshUser.tsx | 4 +- src/app/auth/cookies.ts | 16 ++++++++ src/app/auth/pages/LoginPage.tsx | 4 +- src/app/auth/pages/LogoutPage.tsx | 5 ++- src/app/feed/pages/HomePage.tsx | 4 +- src/app/femtoApp.ts | 8 ++++ src/app/messageBus/messageTypes.ts | 10 ++--- src/app/user/user.ts | 48 ++++++++++++++++++++++ src/app/user/userStore.ts | 38 ----------------- src/components/NavBar.tsx | 4 +- src/types.d.ts | 12 ++++++ src/useRefreshSessionLoop.ts | 28 +++++++++++++ 16 files changed, 148 insertions(+), 107 deletions(-) create mode 100644 src/app/auth/cookies.ts create mode 100644 src/app/femtoApp.ts create mode 100644 src/app/user/user.ts delete mode 100644 src/app/user/userStore.ts create mode 100644 src/useRefreshSessionLoop.ts diff --git a/src/App.tsx b/src/App.tsx index f7a78c6..3e0a263 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,39 +10,17 @@ import LogoutPage from './app/auth/pages/LogoutPage.tsx' import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx' import AdminPage from './app/admin/pages/AdminPage.tsx' import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx' -import { useUser } from './app/user/userStore.ts' -import { useEffect, useMemo } from 'react' +import { initUser } from './app/user/user.ts' +import { useRefreshSessionLoop } from './useRefreshSessionLoop.ts' + +const postService = new PostsService() +const mediaService = new MediaService() +const authService = new AuthService() + +initUser() export default function App() { - const postService = new PostsService() - const mediaService = new MediaService() - const authService = useMemo(() => new AuthService(), []) - - const { user, setUser } = useUser() - - const userId = user?.userId ?? null - - useEffect(() => { - if (userId == null) { - return - } - - const timeouts: number[] = [] - - timeouts.push( - setTimeout(async function refreshUser() { - const userInfo = await authService.refreshUser(userId) - - setUser(userInfo) - - timeouts.push(setTimeout(refreshUser, 60_000)) - }), - ) - - return () => { - timeouts.forEach(clearTimeout) - } - }, [authService, setUser, userId]) + useRefreshSessionLoop(authService) return ( diff --git a/src/app/auth/authService.ts b/src/app/auth/authService.ts index bdd721c..3557c52 100644 --- a/src/app/auth/authService.ts +++ b/src/app/auth/authService.ts @@ -2,7 +2,7 @@ import { dispatchMessage } from '../messageBus/messageBus.ts' import client from '../api/client.ts' import { ProblemDetails } from '../../types' import { SignupCode } from './signupCode.ts' -import { User } from '../user/userStore.ts' +import { getCookie } from './cookies.ts' export class AuthService { constructor() {} @@ -17,7 +17,7 @@ export class AuthService { throw new Error('invalid credentials') } - dispatchMessage('auth:logged-in', { ...res.data }) + dispatchMessage('auth:logged-in', null) } async signup(username: string, password: string, signupCode: string) { @@ -31,7 +31,7 @@ export class AuthService { throw new Error((res.error as ProblemDetails)?.detail ?? 'invalid credentials') } - dispatchMessage('auth:registered', { ...res.data }) + dispatchMessage('auth:registered', null) } async logout() { @@ -65,30 +65,18 @@ export class AuthService { return res.data.signupCodes.map(SignupCode.fromDto) } - async refreshUser(userId: string): Promise { - if (this.getCookie('hasSession') !== 'true') { + async refreshUser(userId: string) { + if (getCookie('hasSession') !== 'true') { return null } - const res = await client.GET(`/auth/user/{userId}`, { + await client.GET(`/auth/user/{userId}`, { params: { path: { userId }, }, credentials: 'include', }) - return res.data ?? null - } - - private getCookie(cookieName: string): string | undefined { - const cookie = document.cookie - .split('; ') - .map((c) => { - const [name, value] = c.split('=') - return { name, value } - }) - .find((c) => c.name === cookieName) - - return cookie?.value + dispatchMessage('auth:refreshed', null) } } diff --git a/src/app/auth/components/AuthNavButtons.tsx b/src/app/auth/components/AuthNavButtons.tsx index 33769c1..6e58d01 100644 --- a/src/app/auth/components/AuthNavButtons.tsx +++ b/src/app/auth/components/AuthNavButtons.tsx @@ -1,9 +1,9 @@ -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' import NavButton from '../../../components/buttons/NavButton.tsx' import { useLocation } from 'react-router-dom' export default function AuthNavButtons() { - const { user } = useUser() + const user = useUser() const { pathname } = useLocation() diff --git a/src/app/auth/components/Protected.tsx b/src/app/auth/components/Protected.tsx index ad72109..f5ea238 100644 --- a/src/app/auth/components/Protected.tsx +++ b/src/app/auth/components/Protected.tsx @@ -1,9 +1,9 @@ -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' import { useNavigate, Outlet } from 'react-router-dom' import { useEffect } from 'react' export default function Protected() { - const { user } = useUser() + const user = useUser() const navigate = useNavigate() diff --git a/src/app/auth/components/RefreshUser.tsx b/src/app/auth/components/RefreshUser.tsx index 34dd268..b49bb1e 100644 --- a/src/app/auth/components/RefreshUser.tsx +++ b/src/app/auth/components/RefreshUser.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren, useEffect, useRef } from 'react' import { AuthService } from '../authService.ts' -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' interface RefreshUserProps { authService: AuthService @@ -10,7 +10,7 @@ export default function RefreshUser({ authService, children, }: PropsWithChildren) { - const { user } = useUser() + const user = useUser() const didRefresh = useRef(false) useEffect(() => { diff --git a/src/app/auth/cookies.ts b/src/app/auth/cookies.ts new file mode 100644 index 0000000..ba861c7 --- /dev/null +++ b/src/app/auth/cookies.ts @@ -0,0 +1,16 @@ +export function getCookie(cookieName: string): string | undefined { + return getCookies().get(cookieName) +} + +export function getCookies(): Map { + return document.cookie + .split('; ') + .map((c) => { + const [name, value] = c.split('=') as [string, string] + return { name, value } + }) + .reduce((acc, c) => { + acc.set(c.name, c.value) + return acc + }, new Map()) +} diff --git a/src/app/auth/pages/LoginPage.tsx b/src/app/auth/pages/LoginPage.tsx index 954a2cf..5163d71 100644 --- a/src/app/auth/pages/LoginPage.tsx +++ b/src/app/auth/pages/LoginPage.tsx @@ -4,7 +4,7 @@ import TextInput from '../../../components/inputs/TextInput.tsx' import Button from '../../../components/buttons/Button.tsx' import { AuthService } from '../authService.ts' import { useNavigate } from 'react-router-dom' -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' import NavBar from '../../../components/NavBar.tsx' import NavButton from '../../../components/buttons/NavButton.tsx' import LinkButton from '../../../components/buttons/LinkButton.tsx' @@ -22,7 +22,7 @@ export default function LoginPage({ authService }: LoginPageProps) { const passwordInputRef = useRef(null) const navigate = useNavigate() - const { user } = useUser() + const user = useUser() useEffect(() => { if (user) { diff --git a/src/app/auth/pages/LogoutPage.tsx b/src/app/auth/pages/LogoutPage.tsx index 8ae4ef4..b0e20b6 100644 --- a/src/app/auth/pages/LogoutPage.tsx +++ b/src/app/auth/pages/LogoutPage.tsx @@ -1,7 +1,7 @@ import { useNavigate } from 'react-router-dom' import { AuthService } from '../authService.ts' import { useEffect } from 'react' -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' interface LogoutPageProps { authService: AuthService @@ -9,9 +9,10 @@ interface LogoutPageProps { export default function LogoutPage({ authService }: LogoutPageProps) { const navigate = useNavigate() - const { user } = useUser() + const user = useUser() useEffect(() => { + console.debug(user) if (!user) { navigate('/login') } diff --git a/src/app/feed/pages/HomePage.tsx b/src/app/feed/pages/HomePage.tsx index 5ba5d27..7abd10e 100644 --- a/src/app/feed/pages/HomePage.tsx +++ b/src/app/feed/pages/HomePage.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react' import FeedView from '../components/FeedView.tsx' import { PostsService } from '../posts/postsService.ts' -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' import { MediaService } from '../../media/mediaService.ts' import NewPostWidget from '../../../components/NewPostWidget.tsx' import { useFeedViewModel } from '../components/FeedView.ts' @@ -18,7 +18,7 @@ interface HomePageProps { } export default function HomePage({ postsService, mediaService }: HomePageProps) { - const { user } = useUser() + const user = useUser() useSaveSignupCodeToLocalStorage() const [isSubmitting, setIsSubmitting] = useState(false) diff --git a/src/app/femtoApp.ts b/src/app/femtoApp.ts new file mode 100644 index 0000000..cc9593a --- /dev/null +++ b/src/app/femtoApp.ts @@ -0,0 +1,8 @@ +import { FemtoApp } from '../types' +import { produce } from 'immer' + +export function setGlobal(k: K, v: FemtoApp[K]) { + window.$femto = produce(window.$femto ?? {}, (draft) => { + draft[k] = v + }) +} diff --git a/src/app/messageBus/messageTypes.ts b/src/app/messageBus/messageTypes.ts index 080964e..2338618 100644 --- a/src/app/messageBus/messageTypes.ts +++ b/src/app/messageBus/messageTypes.ts @@ -1,10 +1,10 @@ -import { User } from '../user/userStore.ts' +import { User } from '../user/user.ts' export interface MessageTypes { - 'auth:logged-in': User - 'auth:registered': User + 'auth:logged-in': null + 'auth:registered': null 'auth:logged-out': null 'auth:unauthorized': null - 'auth:user-refreshed': User - 'auth:user-refresh-failed': null + 'auth:refreshed': null + 'user:updated': User | null } diff --git a/src/app/user/user.ts b/src/app/user/user.ts new file mode 100644 index 0000000..a962607 --- /dev/null +++ b/src/app/user/user.ts @@ -0,0 +1,48 @@ +import { addMessageListener, dispatchMessage } from '../messageBus/messageBus.ts' +import { getCookie } from '../auth/cookies.ts' +import { useMessageListener } from '../../hooks/useMessageListener.ts' +import { useState } from 'react' +import { setGlobal } from '../femtoApp.ts' + +export interface User { + userId: string + username: string + isSuperUser: boolean +} + +let globalUser: User | null + +export function initUser() { + updateUser() +} + +function updateUser() { + globalUser = getUserFromCookie() + console.debug(globalUser) + setGlobal('user', globalUser) + dispatchMessage('user:updated', globalUser) +} + +addMessageListener('auth:logged-in', updateUser) +addMessageListener('auth:registered', updateUser) +addMessageListener('auth:logged-out', updateUser) +addMessageListener('auth:refreshed', updateUser) + +export function useUser(): User | null { + const [user, setUser] = useState(globalUser) + + useMessageListener('user:updated', (u) => { + setUser(u) + }) + + return user +} + +function getUserFromCookie(): User | null { + const userCookie = getCookie('user') + + if (!userCookie) return null + + // TODO validate but it should be fine + return JSON.parse(decodeURIComponent(userCookie)) as User +} diff --git a/src/app/user/userStore.ts b/src/app/user/userStore.ts deleted file mode 100644 index f52d391..0000000 --- a/src/app/user/userStore.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createStore, Store, useStore } from '../../utils/store.ts' -import { addMessageListener } from '../messageBus/messageBus.ts' - -export interface User { - userId: string - username: string - isSuperUser: boolean -} - -export type UserStore = Store - -const UserKey = 'user' - -export const userStore = createStore(loadStoredUser()) - -userStore.subscribe((user) => { - localStorage.setItem(UserKey, JSON.stringify(user)) -}) - -const setUser = (u: User | null) => userStore.setState(u) -addMessageListener('auth:logged-in', setUser) -addMessageListener('auth:registered', setUser) -addMessageListener('auth:logged-out', setUser) - -export const useUser = () => { - const [user, setUser] = useStore(userStore) - - return { user, setUser } -} - -function loadStoredUser(): User | null { - const json = localStorage.getItem(UserKey) - if (json) { - return JSON.parse(json) as User - } else { - return null - } -} diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index 5dc99dd..8127122 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -1,11 +1,11 @@ import { PropsWithChildren } from 'react' -import { useUser } from '../app/user/userStore.ts' +import { useUser } from '../app/user/user.ts' import NavButton from './buttons/NavButton.tsx' type NavBarProps = unknown export default function NavBar({ children }: PropsWithChildren) { - const { user } = useUser() + const user = useUser() return (