Compare commits
6 Commits
1c1f22afe5
...
e80989aecd
| Author | SHA1 | Date | |
|---|---|---|---|
| e80989aecd | |||
| 23798e301b | |||
| f9bd9292ed | |||
| 03201cf133 | |||
| 47cef8e5a5 | |||
| 39c1958128 |
@ -43,6 +43,11 @@ steps:
|
|||||||
build_args:
|
build_args:
|
||||||
- VITE_API_BASE_URL=https://rubble.se/hemhub/api/
|
- VITE_API_BASE_URL=https://rubble.se/hemhub/api/
|
||||||
- VITE_BASE_PATH=/hemhub/app/
|
- VITE_BASE_PATH=/hemhub/app/
|
||||||
|
- VITE_OIDC_AUTHORITY=https://rubble.se/auth/realms/hemhub
|
||||||
|
- VITE_OIDC_CLIENT_ID=hemhub-public
|
||||||
|
- VITE_OIDC_REDIRECT_URI=https://rubble.se/hemhub/app/auth/callback
|
||||||
|
- VITE_OIDC_POST_LOGOUT_REDIRECT_URI=https://rubble.se/hemhub/app/
|
||||||
|
- VITE_OIDC_SILENT_REDIRECT_URI=https://rubble.se/hemhub/app/auth/silent-renew
|
||||||
|
|
||||||
# Taggar (latest + kort SHA)
|
# Taggar (latest + kort SHA)
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
6
.env.local
Normal file
6
.env.local
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
VITE_API_BASE_URL=http://localhost:8080/
|
||||||
|
VITE_OIDC_AUTHORITY=http://localhost:8081/realms/hemhub
|
||||||
|
VITE_OIDC_CLIENT_ID=hemhub-public
|
||||||
|
VITE_OIDC_REDIRECT_URI=http://localhost:5173/auth/callback
|
||||||
|
VITE_OIDC_POST_LOGOUT_REDIRECT_URI=http://localhost:5173/
|
||||||
|
VITE_OIDC_SILENT_REDIRECT_URI=http://localhost:5173/auth/silent-renew
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,7 +11,7 @@ coverage/
|
|||||||
|
|
||||||
# env
|
# env
|
||||||
.env
|
.env
|
||||||
.env.*
|
#.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
43
Dockerfile
43
Dockerfile
@ -1,48 +1,41 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
############################
|
|
||||||
# Build stage
|
|
||||||
############################
|
|
||||||
FROM node:20-alpine AS build
|
FROM node:20-alpine AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# pnpm via Corepack
|
|
||||||
ENV PNPM_HOME="/root/.local/share/pnpm"
|
ENV PNPM_HOME="/root/.local/share/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||||
|
|
||||||
# Installera beroenden
|
|
||||||
COPY package.json pnpm-lock.yaml* ./
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# App-källor
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# --- Build args (styr Vite) ---
|
# --- Build args (med BRA defaults) ---
|
||||||
# Backend-url (kan sättas i Drone secrets)
|
ARG VITE_API_BASE_URL=http://localhost:8080
|
||||||
ARG VITE_API_BASE_URL=https://rubble.se/hemhub/api
|
ARG VITE_BASE_PATH=/
|
||||||
# Base path för proxy under /hemhub/
|
|
||||||
ARG VITE_BASE_PATH=/hemhub/app
|
# Gör ARG:arna synliga för Vite config (vite.config.ts läser process.env.VITE_*)
|
||||||
ENV VITE_API_BASE_URL=https://rubble.se/hemhub/api
|
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
|
||||||
ENV VITE_BASE_PATH=/hemhub/app
|
ENV VITE_BASE_PATH=$VITE_BASE_PATH
|
||||||
|
|
||||||
|
ARG VITE_OIDC_AUTHORITY=
|
||||||
|
ARG VITE_OIDC_CLIENT_ID=
|
||||||
|
ARG VITE_OIDC_REDIRECT_URI=
|
||||||
|
ARG VITE_OIDC_POST_LOGOUT_REDIRECT_URI=
|
||||||
|
ARG VITE_OIDC_SILENT_REDIRECT_URI=
|
||||||
|
ENV VITE_OIDC_AUTHORITY=$VITE_OIDC_AUTHORITY \
|
||||||
|
VITE_OIDC_CLIENT_ID=$VITE_OIDC_CLIENT_ID \
|
||||||
|
VITE_OIDC_REDIRECT_URI=$VITE_OIDC_REDIRECT_URI \
|
||||||
|
VITE_OIDC_POST_LOGOUT_REDIRECT_URI=$VITE_OIDC_POST_LOGOUT_REDIRECT_URI \
|
||||||
|
VITE_OIDC_SILENT_REDIRECT_URI=$VITE_OIDC_SILENT_REDIRECT_URI
|
||||||
|
|
||||||
# Bygg (Vite läser env vid build)
|
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
|
|
||||||
############################
|
|
||||||
# Runtime stage (Nginx)
|
|
||||||
############################
|
|
||||||
FROM nginx:1.27-alpine AS runtime
|
FROM nginx:1.27-alpine AS runtime
|
||||||
|
|
||||||
# Lägg in Nginx-konfig (SPA fallback)
|
|
||||||
COPY ./.docker/nginx.conf /etc/nginx/conf.d/default.conf
|
COPY ./.docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
# Statiska filer från builden
|
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
# Hälsokoll (valfritt)
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD wget -qO- http://127.0.0.1/ || exit 1
|
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD wget -qO- http://127.0.0.1/ || exit 1
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
80
auth/AuthProvider.tsx
Normal file
80
auth/AuthProvider.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
18
auth/RequireAuth.tsx
Normal file
18
auth/RequireAuth.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { PropsWithChildren, useEffect } from 'react'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
|
import { useAuth } from './AuthProvider'
|
||||||
|
|
||||||
|
|
||||||
|
export function RequireAuth({ children }: PropsWithChildren) {
|
||||||
|
const { isAuthenticated, signIn } = useAuth()
|
||||||
|
const loc = useLocation()
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAuthenticated) void signIn(loc.pathname + loc.search)
|
||||||
|
}, [isAuthenticated, signIn, loc])
|
||||||
|
|
||||||
|
|
||||||
|
if (!isAuthenticated) return null
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
19
auth/oidc.ts
Normal file
19
auth/oidc.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { UserManager, WebStorageStateStore, Log, type UserManagerSettings } from 'oidc-client-ts'
|
||||||
|
|
||||||
|
console.log('AUTHORITY:', import.meta.env.VITE_OIDC_AUTHORITY)
|
||||||
|
const settings: UserManagerSettings = {
|
||||||
|
authority: import.meta.env.VITE_OIDC_AUTHORITY!,
|
||||||
|
client_id: import.meta.env.VITE_OIDC_CLIENT_ID!,
|
||||||
|
redirect_uri: import.meta.env.VITE_OIDC_REDIRECT_URI!,
|
||||||
|
post_logout_redirect_uri: import.meta.env.VITE_OIDC_POST_LOGOUT_REDIRECT_URI!,
|
||||||
|
response_type: 'code',
|
||||||
|
scope: 'openid profile email',
|
||||||
|
loadUserInfo: true,
|
||||||
|
automaticSilentRenew: true,
|
||||||
|
silent_redirect_uri: import.meta.env.VITE_OIDC_SILENT_REDIRECT_URI!,
|
||||||
|
userStore: new WebStorageStateStore({ store: window.sessionStorage }),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) Log.setLogger(console)
|
||||||
|
export const userManager = new UserManager(settings)
|
||||||
@ -1,6 +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'
|
||||||
|
|
||||||
export function AppProviders({ children }: PropsWithChildren) {
|
export function AppProviders({ children }: PropsWithChildren) {
|
||||||
const [client] = useState(
|
const [client] = useState(
|
||||||
@ -9,5 +10,9 @@ export function AppProviders({ children }: PropsWithChildren) {
|
|||||||
defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1 } },
|
defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1 } },
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return <QueryClientProvider client={client}>{children}</QueryClientProvider>
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<QueryClientProvider client={client}>{children}</QueryClientProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,22 @@ import { DashboardPage } from '@/pages/DashboardPage'
|
|||||||
import { HouseholdBoardPage } from '@/pages/HouseholdBoardPage'
|
import { HouseholdBoardPage } from '@/pages/HouseholdBoardPage'
|
||||||
import { ProjectBoardPage } from '@/pages/ProjectBoardPage'
|
import { ProjectBoardPage } from '@/pages/ProjectBoardPage'
|
||||||
import { DueTomorrowPage } from '@/pages/DueTomorrowPage'
|
import { DueTomorrowPage } from '@/pages/DueTomorrowPage'
|
||||||
|
import AuthCallbackPage from '@/pages/AuthCallbackPage'
|
||||||
|
import SilentRenewPage from '@/pages/SilentRenewPage'
|
||||||
|
import LogoutPage from '@/pages/LogoutPage'
|
||||||
|
import { RequireAuth } from 'auth/RequireAuth'
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
|
{ path: '/auth/callback', element: <AuthCallbackPage /> },
|
||||||
|
{ path: '/auth/silent-renew', element: <SilentRenewPage /> },
|
||||||
|
{ path: '/logout', element: <LogoutPage /> },
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <RootLayout />,
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<RootLayout />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <DashboardPage /> },
|
{ index: true, element: <DashboardPage /> },
|
||||||
{ path: 'households/:householdId/board', element: <HouseholdBoardPage /> },
|
{ path: 'households/:householdId/board', element: <HouseholdBoardPage /> },
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
// 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 { useAuth } from 'auth/AuthProvider'
|
||||||
|
|
||||||
export function RootLayout() {
|
export function RootLayout() {
|
||||||
|
const { isAuthenticated, signIn } = useAuth()
|
||||||
return (
|
return (
|
||||||
<div className="min-h-dvh bg-zinc-50 text-zinc-900">
|
<div className="min-h-dvh bg-zinc-50 text-zinc-900">
|
||||||
<header className="sticky top-0 border-b bg-white/70 backdrop-blur supports-[backdrop-filter]:bg-white/40">
|
<header className="sticky top-0 border-b bg-white/70 backdrop-blur supports-[backdrop-filter]:bg-white/40">
|
||||||
@ -11,6 +14,12 @@ export function RootLayout() {
|
|||||||
<NavLink to="/" className={({isActive})=>isActive?'font-semibold underline':'opacity-80 hover:opacity-100'}>Dashboard</NavLink>
|
<NavLink to="/" className={({isActive})=>isActive?'font-semibold underline':'opacity-80 hover:opacity-100'}>Dashboard</NavLink>
|
||||||
<NavLink to="/tasks/due-tomorrow" className={({isActive})=>isActive?'font-semibold underline':'opacity-80 hover:opacity-100'}>Due tomorrow</NavLink>
|
<NavLink to="/tasks/due-tomorrow" className={({isActive})=>isActive?'font-semibold underline':'opacity-80 hover:opacity-100'}>Due tomorrow</NavLink>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div className="ml-auto flex items-center gap-3">
|
||||||
|
<MeBadge />
|
||||||
|
{isAuthenticated
|
||||||
|
? <NavLink to="/logout" className="text-sm underline">Logga ut</NavLink>
|
||||||
|
: <button onClick={()=>signIn(window.location.pathname+window.location.search)} className="text-sm underline">Logga in</button>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="mx-auto max-w-6xl px-4 py-6">
|
<main className="mx-auto max-w-6xl px-4 py-6">
|
||||||
|
|||||||
17
src/features/me/MeBadge.tsx
Normal file
17
src/features/me/MeBadge.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { fetchMe } from './api'
|
||||||
|
|
||||||
|
export function MeBadge() {
|
||||||
|
const hasToken = !!sessionStorage.getItem('access_token')
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ['me'],
|
||||||
|
queryFn: fetchMe,
|
||||||
|
enabled: hasToken, // 👈 vänta tills token finns
|
||||||
|
retry: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!hasToken) return <span className="opacity-60">ej inloggad</span>
|
||||||
|
if (isLoading) return <span className="opacity-60">…</span>
|
||||||
|
if (isError) return <span className="opacity-60">fel</span>
|
||||||
|
return <span className="opacity-80 text-sm">{data?.name || data?.preferred_username || 'me'}</span>
|
||||||
|
}
|
||||||
3
src/features/me/api.ts
Normal file
3
src/features/me/api.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { api } from '@/lib/http'
|
||||||
|
export type Me = { name?: string; preferred_username?: string; email?: string }
|
||||||
|
export const fetchMe = () => api.get('me').json<Me>()
|
||||||
23
src/lib/http.ts
Normal file
23
src/lib/http.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// src/lib/http.ts
|
||||||
|
import ky from 'ky'
|
||||||
|
|
||||||
|
export const api = ky.create({
|
||||||
|
// pekar på ditt API, t.ex. https://rubble.se/hemhub/api/ i prod (sätts via VITE_API_BASE_URL)
|
||||||
|
prefixUrl: import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8080/',
|
||||||
|
hooks: {
|
||||||
|
beforeRequest: [
|
||||||
|
async (req) => {
|
||||||
|
const token = sessionStorage.getItem('access_token')
|
||||||
|
if (token) req.headers.set('Authorization', `Bearer ${token}`)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
afterResponse: [
|
||||||
|
async (_req, _opts, res) => {
|
||||||
|
if (res.status === 401) {
|
||||||
|
// I Iteration 1, mjuk hantering — vi låter RequireAuth sköta redirecten
|
||||||
|
console.warn('401 from API – probably not logged in yet')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
4
src/lib/queryKeys.ts
Normal file
4
src/lib/queryKeys.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// src/lib/queryKeys.ts
|
||||||
|
export const qk = {
|
||||||
|
me: ['me'] as const,
|
||||||
|
}
|
||||||
29
src/pages/AuthCallbackPage.tsx
Normal file
29
src/pages/AuthCallbackPage.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// src/pages/AuthCallbackPage.tsx
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { userManager } from 'auth/oidc'
|
||||||
|
|
||||||
|
export default function AuthCallbackPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const handled = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (handled.current) return // 👈 skydd mot StrictMode dubbelkörning
|
||||||
|
handled.current = true
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
try {
|
||||||
|
const res = await userManager.signinRedirectCallback()
|
||||||
|
const target = (res?.state as any)?.returnTo || '/'
|
||||||
|
// Städa bort ?code&state ur URL:en:
|
||||||
|
window.history.replaceState({}, '', target)
|
||||||
|
navigate(target, { replace: true })
|
||||||
|
} catch (err) {
|
||||||
|
console.error('signinRedirectCallback failed:', err)
|
||||||
|
navigate('/', { replace: true })
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, [navigate])
|
||||||
|
|
||||||
|
return <p>Completing sign-in…</p>
|
||||||
|
}
|
||||||
7
src/pages/LogoutPage.tsx
Normal file
7
src/pages/LogoutPage.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useAuth } from 'auth/AuthProvider'
|
||||||
|
export default function LogoutPage() {
|
||||||
|
const { signOut } = useAuth()
|
||||||
|
useEffect(() => { void signOut() }, [signOut])
|
||||||
|
return <p>Signing out…</p>
|
||||||
|
}
|
||||||
8
src/pages/SilentRenewPage.tsx
Normal file
8
src/pages/SilentRenewPage.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { userManager } from 'auth/oidc'
|
||||||
|
|
||||||
|
|
||||||
|
export default function SilentRenewPage() {
|
||||||
|
useEffect(() => { userManager.signinSilentCallback() }, [])
|
||||||
|
return <p>Silent renew…</p>
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
|
|||||||
Reference in New Issue
Block a user