moved Auth into src
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Urban Modig
2025-10-15 09:32:55 +02:00
parent ffb484b117
commit b99fc6d348
12 changed files with 74 additions and 87 deletions

View File

@ -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
}

View File

@ -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(

View File

@ -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
View 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>
}

View File

@ -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
View 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
View 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
}

View File

@ -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()

View File

@ -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 {

View File

@ -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])

View File

@ -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() {