This commit is contained in:
@ -1,80 +0,0 @@
|
|||||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
|
||||||
import type { PropsWithChildren } from 'react'
|
|
||||||
import { userManager } from './oidc'
|
|
||||||
import type { User } from 'oidc-client-ts'
|
|
||||||
|
|
||||||
|
|
||||||
interface AuthCtx {
|
|
||||||
user: User | null
|
|
||||||
isAuthenticated: boolean
|
|
||||||
signIn: (returnTo?: string) => Promise<void>
|
|
||||||
signOut: () => Promise<void>
|
|
||||||
getAccessToken: () => string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const Ctx = createContext<AuthCtx | null>(null)
|
|
||||||
|
|
||||||
|
|
||||||
export function AuthProvider({ children }: PropsWithChildren) {
|
|
||||||
const [user, setUser] = useState<User | null>(null)
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
userManager.getUser().then(u => setUser(u))
|
|
||||||
|
|
||||||
|
|
||||||
const onLoaded = (u: User) => setUser(u)
|
|
||||||
const onUnloaded = () => setUser(null)
|
|
||||||
const onExpired = async () => {
|
|
||||||
try { await userManager.signinSilent() } catch {/* ignore */}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
userManager.events.addUserLoaded(onLoaded)
|
|
||||||
userManager.events.addUserUnloaded(onUnloaded)
|
|
||||||
userManager.events.addAccessTokenExpired(onExpired)
|
|
||||||
return () => {
|
|
||||||
userManager.events.removeUserLoaded(onLoaded)
|
|
||||||
userManager.events.removeUserUnloaded(onUnloaded)
|
|
||||||
userManager.events.removeAccessTokenExpired(onExpired)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
|
|
||||||
// Spegla token till sessionStorage så ky kan läsa den
|
|
||||||
// i useEffect som speglar token:
|
|
||||||
useEffect(() => {
|
|
||||||
const token = user?.access_token ?? null
|
|
||||||
if (token) {
|
|
||||||
sessionStorage.setItem('access_token', token)
|
|
||||||
console.debug('access_token set')
|
|
||||||
} else {
|
|
||||||
sessionStorage.removeItem('access_token')
|
|
||||||
}
|
|
||||||
}, [user])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const api: AuthCtx = useMemo(() => ({
|
|
||||||
user,
|
|
||||||
isAuthenticated: !!user && !user.expired,
|
|
||||||
signIn: async (returnTo) => {
|
|
||||||
await userManager.signinRedirect({ state: { returnTo } })
|
|
||||||
},
|
|
||||||
signOut: async () => {
|
|
||||||
await userManager.signoutRedirect()
|
|
||||||
},
|
|
||||||
getAccessToken: () => user?.access_token ?? null,
|
|
||||||
}), [user])
|
|
||||||
|
|
||||||
|
|
||||||
return <Ctx.Provider value={api}>{children}</Ctx.Provider>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function useAuth() {
|
|
||||||
const ctx = useContext(Ctx)
|
|
||||||
if (!ctx) throw new Error('useAuth must be used within <AuthProvider>')
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// src/app/providers.tsx
|
// src/app/providers.tsx
|
||||||
import { type PropsWithChildren, useState } from 'react'
|
import { type PropsWithChildren, useState } from 'react'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { AuthProvider } from 'auth/AuthProvider'
|
import AuthProvider from '@/auth/AuthProvider'
|
||||||
|
|
||||||
export function AppProviders({ children }: PropsWithChildren) {
|
export function AppProviders({ children }: PropsWithChildren) {
|
||||||
const [client] = useState(
|
const [client] = useState(
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { DueTomorrowPage } from '@/pages/DueTomorrowPage'
|
|||||||
import AuthCallbackPage from '@/pages/AuthCallbackPage'
|
import AuthCallbackPage from '@/pages/AuthCallbackPage'
|
||||||
import SilentRenewPage from '@/pages/SilentRenewPage'
|
import SilentRenewPage from '@/pages/SilentRenewPage'
|
||||||
import LogoutPage from '@/pages/LogoutPage'
|
import LogoutPage from '@/pages/LogoutPage'
|
||||||
import { RequireAuth } from 'auth/RequireAuth'
|
import { RequireAuth } from '@/auth/RequireAuth'
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{ path: '/auth/callback', element: <AuthCallbackPage /> },
|
{ path: '/auth/callback', element: <AuthCallbackPage /> },
|
||||||
|
|||||||
47
src/auth/AuthProvider.tsx
Normal file
47
src/auth/AuthProvider.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect, useMemo, useState, type PropsWithChildren } from 'react'
|
||||||
|
import type { User } from 'oidc-client-ts'
|
||||||
|
import { userManager } from '@/auth/oidc'
|
||||||
|
import { Ctx, type AuthCtx } from './context'
|
||||||
|
|
||||||
|
export default function AuthProvider({ children }: PropsWithChildren) {
|
||||||
|
const [user, setUser] = useState<User | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
userManager.getUser().then(u => setUser(u))
|
||||||
|
|
||||||
|
const onLoaded = (u: User) => setUser(u)
|
||||||
|
const onUnloaded = () => setUser(null)
|
||||||
|
const onExpired = async () => {
|
||||||
|
try { await userManager.signinSilent() } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
userManager.events.addUserLoaded(onLoaded)
|
||||||
|
userManager.events.addUserUnloaded(onUnloaded)
|
||||||
|
userManager.events.addAccessTokenExpired(onExpired)
|
||||||
|
return () => {
|
||||||
|
userManager.events.removeUserLoaded(onLoaded)
|
||||||
|
userManager.events.removeUserUnloaded(onUnloaded)
|
||||||
|
userManager.events.removeAccessTokenExpired(onExpired)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = user?.access_token ?? null
|
||||||
|
if (token) sessionStorage.setItem('access_token', token)
|
||||||
|
else sessionStorage.removeItem('access_token')
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
const api: AuthCtx = useMemo(() => ({
|
||||||
|
user,
|
||||||
|
isAuthenticated: !!user && !user.expired,
|
||||||
|
signIn: async (returnTo?: string) => {
|
||||||
|
await userManager.signinRedirect({ state: { returnTo } })
|
||||||
|
},
|
||||||
|
signOut: async () => {
|
||||||
|
await userManager.signoutRedirect()
|
||||||
|
},
|
||||||
|
getAccessToken: () => user?.access_token ?? null,
|
||||||
|
}), [user])
|
||||||
|
|
||||||
|
return <Ctx.Provider value={api}>{children}</Ctx.Provider>
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { PropsWithChildren } from 'react'
|
import type { PropsWithChildren } from 'react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { useAuth } from './AuthProvider'
|
import { useAuth } from './useAuth'
|
||||||
|
|
||||||
|
|
||||||
export function RequireAuth({ children }: PropsWithChildren) {
|
export function RequireAuth({ children }: PropsWithChildren) {
|
||||||
12
src/auth/context.ts
Normal file
12
src/auth/context.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { createContext } from 'react'
|
||||||
|
import type { User } from 'oidc-client-ts'
|
||||||
|
|
||||||
|
export interface AuthCtx {
|
||||||
|
user: User | null
|
||||||
|
isAuthenticated: boolean
|
||||||
|
signIn: (returnTo?: string) => Promise<void>
|
||||||
|
signOut: () => Promise<void>
|
||||||
|
getAccessToken: () => string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Ctx = createContext<AuthCtx | null>(null)
|
||||||
8
src/auth/useAuth.ts
Normal file
8
src/auth/useAuth.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { useContext } from 'react'
|
||||||
|
import { Ctx } from './context'
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const ctx = useContext(Ctx)
|
||||||
|
if (!ctx) throw new Error('useAuth must be used within <AuthProvider>')
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// src/components/layout/RootLayout.tsx
|
// src/components/layout/RootLayout.tsx
|
||||||
import { Outlet, NavLink } from 'react-router-dom'
|
import { Outlet, NavLink } from 'react-router-dom'
|
||||||
import { MeBadge } from '../../features/me/MeBadge'
|
import { MeBadge } from '../../features/me/MeBadge'
|
||||||
import { useAuth } from 'auth/AuthProvider'
|
import { useAuth } from '@/auth/useAuth'
|
||||||
|
|
||||||
export function RootLayout() {
|
export function RootLayout() {
|
||||||
const { isAuthenticated, signIn } = useAuth()
|
const { isAuthenticated, signIn } = useAuth()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// src/pages/AuthCallbackPage.tsx
|
// src/pages/AuthCallbackPage.tsx
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { userManager } from 'auth/oidc' // behåll alias/relativt enligt ditt projekt
|
import { userManager } from '@/auth/oidc' // behåll alias/relativt enligt ditt projekt
|
||||||
|
|
||||||
// Hjälpare: plocka ut returnTo från okänt state
|
// Hjälpare: plocka ut returnTo från okänt state
|
||||||
function pickReturnTo(state: unknown): string | undefined {
|
function pickReturnTo(state: unknown): string | undefined {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { useAuth } from '@/auth/useAuth'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useAuth } from 'auth/AuthProvider'
|
|
||||||
export default function LogoutPage() {
|
export default function LogoutPage() {
|
||||||
const { signOut } = useAuth()
|
const { signOut } = useAuth()
|
||||||
useEffect(() => { void signOut() }, [signOut])
|
useEffect(() => { void signOut() }, [signOut])
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { userManager } from 'auth/oidc'
|
import { userManager } from '@/auth/oidc'
|
||||||
|
|
||||||
|
|
||||||
export default function SilentRenewPage() {
|
export default function SilentRenewPage() {
|
||||||
|
|||||||
Reference in New Issue
Block a user