mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
fix(web): consume OIDC access_token and add dual SSO sign-out
This commit is contained in:
@@ -103,6 +103,8 @@ export interface User {
|
|||||||
account_permissions: Record<number, string[]>
|
account_permissions: Record<number, string[]>
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
|
sso_id?: string | null;
|
||||||
|
sso_provider?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Theme = 'dark' | 'light'
|
type Theme = 'dark' | 'light'
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import { useNavigate, useLocation } from '@tanstack/react-router'
|
|||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
import { resetToken } from '@/stores/authStore'
|
import { resetToken } from '@/stores/authStore'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||||
|
import { useEdition } from '@/hooks/use-edition'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
interface SignOutDialogProps {
|
interface SignOutDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -31,9 +34,17 @@ export function SignOutDialog({ open, onOpenChange }: SignOutDialogProps) {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const handleSignOut = () => {
|
const { user } = useCurrentUser()
|
||||||
resetToken()
|
const { isPro, features } = useEdition()
|
||||||
const currentPath = location.href
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const isSsoUser =
|
||||||
|
isPro &&
|
||||||
|
features.includes('sso') &&
|
||||||
|
!!user?.sso_provider &&
|
||||||
|
user.sso_provider !== ''
|
||||||
|
|
||||||
|
const goToSignIn = (currentPath: string) => {
|
||||||
navigate({
|
navigate({
|
||||||
to: '/sign-in',
|
to: '/sign-in',
|
||||||
search: { redirect: currentPath },
|
search: { redirect: currentPath },
|
||||||
@@ -41,19 +52,73 @@ export function SignOutDialog({ open, onOpenChange }: SignOutDialogProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localSignOut = () => {
|
||||||
|
resetToken()
|
||||||
|
goToSignIn(location.href)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (!isSsoUser) {
|
||||||
|
localSignOut()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsLoading(true)
|
||||||
|
// Only sign out of bichon; keep the SSO session for one-click sign-in.
|
||||||
|
fetch('/api/auth/oidc/local-logout', { redirect: 'follow' })
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
localSignOut()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
title={t('sign_out.title', 'Sign out')}
|
title={t('sign_out.title', 'Sign out')}
|
||||||
desc={t(
|
desc={
|
||||||
|
isSsoUser
|
||||||
|
? t(
|
||||||
|
'sign_out.sso_desc',
|
||||||
|
'Signing out of Bichon only keeps your SSO session (e.g. Keycloak) active. For full security, choose "Sign out and end SSO session".'
|
||||||
|
)
|
||||||
|
: t(
|
||||||
'sign_out.desc',
|
'sign_out.desc',
|
||||||
'Are you sure you want to sign out? You will need to sign in again to access your account.'
|
'Are you sure you want to sign out? You will need to sign in again to access your account.'
|
||||||
)}
|
)
|
||||||
confirmText={t('sign_out.confirm', 'Sign out')}
|
}
|
||||||
|
confirmText={
|
||||||
|
isSsoUser
|
||||||
|
? t('sign_out.confirm_sso', 'Sign out of Bichon only')
|
||||||
|
: t('sign_out.confirm', 'Sign out')
|
||||||
|
}
|
||||||
destructive
|
destructive
|
||||||
handleConfirm={handleSignOut}
|
isLoading={isLoading}
|
||||||
|
handleConfirm={handleConfirm}
|
||||||
className="sm:max-w-sm"
|
className="sm:max-w-sm"
|
||||||
/>
|
>
|
||||||
|
{isSsoUser && (
|
||||||
|
<div className='grid gap-2'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='inline-flex h-10 items-center justify-center gap-2 rounded-md border border-destructive/50 bg-background px-4 text-sm font-medium text-destructive transition-colors hover:bg-destructive/10'
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={() => {
|
||||||
|
resetToken()
|
||||||
|
window.location.href = '/api/auth/oidc/logout'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('sign_out.full_sign_out', 'Sign out and end SSO session')}
|
||||||
|
</button>
|
||||||
|
<p className='text-muted-foreground px-2 text-xs'>
|
||||||
|
{t(
|
||||||
|
'sign_out.sso_warning',
|
||||||
|
'This will end your SSO session (e.g. Keycloak) and sign you out of all applications using it.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ConfirmDialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,11 +53,12 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
|||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isPro } = useEdition()
|
|
||||||
|
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const redirect = toSearchParams(search).get('redirect') || '/';
|
const redirect = toSearchParams(search).get('redirect') || '/';
|
||||||
|
|
||||||
|
const { isPro, features } = useEdition()
|
||||||
|
const ssoEnabled = isPro && features.includes('sso')
|
||||||
|
|
||||||
const formSchema = getFormSchema(t)
|
const formSchema = getFormSchema(t)
|
||||||
const form = useForm<LoginFormValues>({
|
const form = useForm<LoginFormValues>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@@ -159,7 +160,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
|||||||
{t('auth.login')}
|
{t('auth.login')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isPro && (
|
{ssoEnabled && (
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='mt-2'
|
className='mt-2'
|
||||||
|
|||||||
+11
-1
@@ -26,7 +26,7 @@ import {
|
|||||||
QueryClientProvider,
|
QueryClientProvider,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||||
import { resetToken } from '@/stores/authStore'
|
import { resetToken, setToken } from '@/stores/authStore'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { ThemeProvider } from './context/theme-context'
|
import { ThemeProvider } from './context/theme-context'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
@@ -117,6 +117,16 @@ const queryClient = new QueryClient({
|
|||||||
|
|
||||||
const basepath = (window as any).__BICHON_BASE__ || '/';
|
const basepath = (window as any).__BICHON_BASE__ || '/';
|
||||||
console.log('Current Basepath:', basepath);
|
console.log('Current Basepath:', basepath);
|
||||||
|
|
||||||
|
// OIDC SSO callback: the Pro server redirects back with ?access_token=.
|
||||||
|
// Store it and strip it from the URL before the router/auth flow runs.
|
||||||
|
const ssoToken = new URLSearchParams(window.location.search).get('access_token');
|
||||||
|
if (ssoToken) {
|
||||||
|
setToken({ success: true, access_token: ssoToken });
|
||||||
|
const clean = `${window.location.pathname}${window.location.hash}`;
|
||||||
|
window.history.replaceState(null, '', clean);
|
||||||
|
}
|
||||||
|
|
||||||
// Create a new router instance
|
// Create a new router instance
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
|
|||||||
Reference in New Issue
Block a user