use user from session
This commit is contained in:
parent
5f47162a50
commit
700eaf3eb2
16 changed files with 148 additions and 107 deletions
40
src/App.tsx
40
src/App.tsx
|
@ -10,39 +10,17 @@ import LogoutPage from './app/auth/pages/LogoutPage.tsx'
|
||||||
import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx'
|
import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx'
|
||||||
import AdminPage from './app/admin/pages/AdminPage.tsx'
|
import AdminPage from './app/admin/pages/AdminPage.tsx'
|
||||||
import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx'
|
import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx'
|
||||||
import { useUser } from './app/user/userStore.ts'
|
import { initUser } from './app/user/user.ts'
|
||||||
import { useEffect, useMemo } from 'react'
|
import { useRefreshSessionLoop } from './useRefreshSessionLoop.ts'
|
||||||
|
|
||||||
|
const postService = new PostsService()
|
||||||
|
const mediaService = new MediaService()
|
||||||
|
const authService = new AuthService()
|
||||||
|
|
||||||
|
initUser()
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const postService = new PostsService()
|
useRefreshSessionLoop(authService)
|
||||||
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])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { dispatchMessage } from '../messageBus/messageBus.ts'
|
||||||
import client from '../api/client.ts'
|
import client from '../api/client.ts'
|
||||||
import { ProblemDetails } from '../../types'
|
import { ProblemDetails } from '../../types'
|
||||||
import { SignupCode } from './signupCode.ts'
|
import { SignupCode } from './signupCode.ts'
|
||||||
import { User } from '../user/userStore.ts'
|
import { getCookie } from './cookies.ts'
|
||||||
|
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
@ -17,7 +17,7 @@ export class AuthService {
|
||||||
throw new Error('invalid credentials')
|
throw new Error('invalid credentials')
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchMessage('auth:logged-in', { ...res.data })
|
dispatchMessage('auth:logged-in', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
async signup(username: string, password: string, signupCode: string) {
|
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')
|
throw new Error((res.error as ProblemDetails)?.detail ?? 'invalid credentials')
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchMessage('auth:registered', { ...res.data })
|
dispatchMessage('auth:registered', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
|
@ -65,30 +65,18 @@ export class AuthService {
|
||||||
return res.data.signupCodes.map(SignupCode.fromDto)
|
return res.data.signupCodes.map(SignupCode.fromDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshUser(userId: string): Promise<User | null> {
|
async refreshUser(userId: string) {
|
||||||
if (this.getCookie('hasSession') !== 'true') {
|
if (getCookie('hasSession') !== 'true') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await client.GET(`/auth/user/{userId}`, {
|
await client.GET(`/auth/user/{userId}`, {
|
||||||
params: {
|
params: {
|
||||||
path: { userId },
|
path: { userId },
|
||||||
},
|
},
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.data ?? null
|
dispatchMessage('auth:refreshed', 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useUser } from '../../user/userStore.ts'
|
import { useUser } from '../../user/user.ts'
|
||||||
import NavButton from '../../../components/buttons/NavButton.tsx'
|
import NavButton from '../../../components/buttons/NavButton.tsx'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
export default function AuthNavButtons() {
|
export default function AuthNavButtons() {
|
||||||
const { user } = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useUser } from '../../user/userStore.ts'
|
import { useUser } from '../../user/user.ts'
|
||||||
import { useNavigate, Outlet } from 'react-router-dom'
|
import { useNavigate, Outlet } from 'react-router-dom'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function Protected() {
|
export default function Protected() {
|
||||||
const { user } = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { PropsWithChildren, useEffect, useRef } from 'react'
|
import { PropsWithChildren, useEffect, useRef } from 'react'
|
||||||
import { AuthService } from '../authService.ts'
|
import { AuthService } from '../authService.ts'
|
||||||
import { useUser } from '../../user/userStore.ts'
|
import { useUser } from '../../user/user.ts'
|
||||||
|
|
||||||
interface RefreshUserProps {
|
interface RefreshUserProps {
|
||||||
authService: AuthService
|
authService: AuthService
|
||||||
|
@ -10,7 +10,7 @@ export default function RefreshUser({
|
||||||
authService,
|
authService,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<RefreshUserProps>) {
|
}: PropsWithChildren<RefreshUserProps>) {
|
||||||
const { user } = useUser()
|
const user = useUser()
|
||||||
const didRefresh = useRef(false)
|
const didRefresh = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
16
src/app/auth/cookies.ts
Normal file
16
src/app/auth/cookies.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export function getCookie(cookieName: string): string | undefined {
|
||||||
|
return getCookies().get(cookieName)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCookies(): Map<string, string> {
|
||||||
|
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<string, string>())
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import TextInput from '../../../components/inputs/TextInput.tsx'
|
||||||
import Button from '../../../components/buttons/Button.tsx'
|
import Button from '../../../components/buttons/Button.tsx'
|
||||||
import { AuthService } from '../authService.ts'
|
import { AuthService } from '../authService.ts'
|
||||||
import { useNavigate } from 'react-router-dom'
|
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 NavBar from '../../../components/NavBar.tsx'
|
||||||
import NavButton from '../../../components/buttons/NavButton.tsx'
|
import NavButton from '../../../components/buttons/NavButton.tsx'
|
||||||
import LinkButton from '../../../components/buttons/LinkButton.tsx'
|
import LinkButton from '../../../components/buttons/LinkButton.tsx'
|
||||||
|
@ -22,7 +22,7 @@ export default function LoginPage({ authService }: LoginPageProps) {
|
||||||
const passwordInputRef = useRef<HTMLInputElement | null>(null)
|
const passwordInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { user } = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { AuthService } from '../authService.ts'
|
import { AuthService } from '../authService.ts'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useUser } from '../../user/userStore.ts'
|
import { useUser } from '../../user/user.ts'
|
||||||
|
|
||||||
interface LogoutPageProps {
|
interface LogoutPageProps {
|
||||||
authService: AuthService
|
authService: AuthService
|
||||||
|
@ -9,9 +9,10 @@ interface LogoutPageProps {
|
||||||
|
|
||||||
export default function LogoutPage({ authService }: LogoutPageProps) {
|
export default function LogoutPage({ authService }: LogoutPageProps) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { user } = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.debug(user)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
navigate('/login')
|
navigate('/login')
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import FeedView from '../components/FeedView.tsx'
|
import FeedView from '../components/FeedView.tsx'
|
||||||
import { PostsService } from '../posts/postsService.ts'
|
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 { MediaService } from '../../media/mediaService.ts'
|
||||||
import NewPostWidget from '../../../components/NewPostWidget.tsx'
|
import NewPostWidget from '../../../components/NewPostWidget.tsx'
|
||||||
import { useFeedViewModel } from '../components/FeedView.ts'
|
import { useFeedViewModel } from '../components/FeedView.ts'
|
||||||
|
@ -18,7 +18,7 @@ interface HomePageProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HomePage({ postsService, mediaService }: HomePageProps) {
|
export default function HomePage({ postsService, mediaService }: HomePageProps) {
|
||||||
const { user } = useUser()
|
const user = useUser()
|
||||||
useSaveSignupCodeToLocalStorage()
|
useSaveSignupCodeToLocalStorage()
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
|
8
src/app/femtoApp.ts
Normal file
8
src/app/femtoApp.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { FemtoApp } from '../types'
|
||||||
|
import { produce } from 'immer'
|
||||||
|
|
||||||
|
export function setGlobal<K extends keyof FemtoApp>(k: K, v: FemtoApp[K]) {
|
||||||
|
window.$femto = produce(window.$femto ?? {}, (draft) => {
|
||||||
|
draft[k] = v
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
import { User } from '../user/userStore.ts'
|
import { User } from '../user/user.ts'
|
||||||
|
|
||||||
export interface MessageTypes {
|
export interface MessageTypes {
|
||||||
'auth:logged-in': User
|
'auth:logged-in': null
|
||||||
'auth:registered': User
|
'auth:registered': null
|
||||||
'auth:logged-out': null
|
'auth:logged-out': null
|
||||||
'auth:unauthorized': null
|
'auth:unauthorized': null
|
||||||
'auth:user-refreshed': User
|
'auth:refreshed': null
|
||||||
'auth:user-refresh-failed': null
|
'user:updated': User | null
|
||||||
}
|
}
|
||||||
|
|
48
src/app/user/user.ts
Normal file
48
src/app/user/user.ts
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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<User | null>
|
|
||||||
|
|
||||||
const UserKey = 'user'
|
|
||||||
|
|
||||||
export const userStore = createStore<User | null>(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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { PropsWithChildren } from 'react'
|
import { PropsWithChildren } from 'react'
|
||||||
import { useUser } from '../app/user/userStore.ts'
|
import { useUser } from '../app/user/user.ts'
|
||||||
import NavButton from './buttons/NavButton.tsx'
|
import NavButton from './buttons/NavButton.tsx'
|
||||||
|
|
||||||
type NavBarProps = unknown
|
type NavBarProps = unknown
|
||||||
|
|
||||||
export default function NavBar({ children }: PropsWithChildren<NavBarProps>) {
|
export default function NavBar({ children }: PropsWithChildren<NavBarProps>) {
|
||||||
const { user } = useUser()
|
const user = useUser()
|
||||||
return (
|
return (
|
||||||
<nav className={`w-full flex flex-row justify-end gap-4 px-4 md:px-8 py-3`}>
|
<nav className={`w-full flex flex-row justify-end gap-4 px-4 md:px-8 py-3`}>
|
||||||
{children}
|
{children}
|
||||||
|
|
12
src/types.d.ts
vendored
12
src/types.d.ts
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
import { User } from './app/user/user.ts'
|
||||||
|
|
||||||
export interface ProblemDetails {
|
export interface ProblemDetails {
|
||||||
detail: string
|
detail: string
|
||||||
title: string
|
title: string
|
||||||
|
@ -5,3 +7,13 @@ export interface ProblemDetails {
|
||||||
type: string
|
type: string
|
||||||
traceId: string
|
traceId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
$femto: FemtoApp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FemtoApp {
|
||||||
|
user: User | null
|
||||||
|
}
|
||||||
|
|
28
src/useRefreshSessionLoop.ts
Normal file
28
src/useRefreshSessionLoop.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useUser } from './app/user/user.ts'
|
||||||
|
import { AuthService } from './app/auth/authService.ts'
|
||||||
|
|
||||||
|
// Starts a loop that pings the server to keep the session alive, while also getting any updates on the user profile
|
||||||
|
export function useRefreshSessionLoop(authService: AuthService) {
|
||||||
|
const user = useUser()
|
||||||
|
const userId = user?.userId ?? null
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeouts: number[] = []
|
||||||
|
|
||||||
|
timeouts.push(
|
||||||
|
setTimeout(async function refreshUser() {
|
||||||
|
await authService.refreshUser(userId)
|
||||||
|
timeouts.push(setTimeout(refreshUser, 60_000))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
timeouts.forEach(clearTimeout)
|
||||||
|
}
|
||||||
|
}, [authService, userId])
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue