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[]>
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
sso_id?: string | null;
|
||||
sso_provider?: string | null;
|
||||
}
|
||||
|
||||
type Theme = 'dark' | 'light'
|
||||
|
||||
@@ -21,6 +21,9 @@ import { useNavigate, useLocation } from '@tanstack/react-router'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { resetToken } from '@/stores/authStore'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||
import { useEdition } from '@/hooks/use-edition'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface SignOutDialogProps {
|
||||
open: boolean
|
||||
@@ -31,9 +34,17 @@ export function SignOutDialog({ open, onOpenChange }: SignOutDialogProps) {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { t } = useTranslation()
|
||||
const handleSignOut = () => {
|
||||
resetToken()
|
||||
const currentPath = location.href
|
||||
const { user } = useCurrentUser()
|
||||
const { isPro, features } = useEdition()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const isSsoUser =
|
||||
isPro &&
|
||||
features.includes('sso') &&
|
||||
!!user?.sso_provider &&
|
||||
user.sso_provider !== ''
|
||||
|
||||
const goToSignIn = (currentPath: string) => {
|
||||
navigate({
|
||||
to: '/sign-in',
|
||||
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 (
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t('sign_out.title', 'Sign out')}
|
||||
desc={t(
|
||||
'sign_out.desc',
|
||||
'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')}
|
||||
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',
|
||||
'Are you sure you want to sign out? You will need to sign in again to access your account.'
|
||||
)
|
||||
}
|
||||
confirmText={
|
||||
isSsoUser
|
||||
? t('sign_out.confirm_sso', 'Sign out of Bichon only')
|
||||
: t('sign_out.confirm', 'Sign out')
|
||||
}
|
||||
destructive
|
||||
handleConfirm={handleSignOut}
|
||||
isLoading={isLoading}
|
||||
handleConfirm={handleConfirm}
|
||||
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 navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
const { isPro } = useEdition()
|
||||
|
||||
const { search } = useLocation();
|
||||
const redirect = toSearchParams(search).get('redirect') || '/';
|
||||
|
||||
const { isPro, features } = useEdition()
|
||||
const ssoEnabled = isPro && features.includes('sso')
|
||||
|
||||
const formSchema = getFormSchema(t)
|
||||
const form = useForm<LoginFormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -159,7 +160,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
{t('auth.login')}
|
||||
</Button>
|
||||
|
||||
{isPro && (
|
||||
{ssoEnabled && (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='mt-2'
|
||||
|
||||
+11
-1
@@ -26,7 +26,7 @@ import {
|
||||
QueryClientProvider,
|
||||
} from '@tanstack/react-query'
|
||||
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 { ThemeProvider } from './context/theme-context'
|
||||
import './index.css'
|
||||
@@ -117,6 +117,16 @@ const queryClient = new QueryClient({
|
||||
|
||||
const basepath = (window as any).__BICHON_BASE__ || '/';
|
||||
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
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
|
||||
Reference in New Issue
Block a user