feat: AYON single-sign-on (ticket exchange, task boards, browser entry)
- POST /api/auth/ayon/exchange: redeem single-use ticket (issued by the AYON addon) via AYON_EXCHANGE_URL, mint session JWT, get-or-create internal user row and the <project>/<task> board in the AYON collection - db: getOrCreateAyonUser / getOrCreateAyonBoard / grantAyonCollectionAccess - frontend: /b route redeems ticket from URL and forwards to the board - password login/register paths untouched (legacy instance support)
This commit is contained in:
@@ -6,6 +6,7 @@ import CollectionList from './pages/CollectionList';
|
||||
import CollectionDetail from './pages/CollectionDetail';
|
||||
import Editor from './pages/Editor';
|
||||
import Admin from './pages/Admin';
|
||||
import AyonEntry from './pages/AyonEntry';
|
||||
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const { user, loading } = useAuth();
|
||||
@@ -32,6 +33,7 @@ function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/b" element={<AyonEntry />} />
|
||||
<Route path="/" element={<ProtectedRoute><CollectionList /></ProtectedRoute>} />
|
||||
<Route path="/collection/:collectionId" element={<ProtectedRoute><CollectionDetail /></ProtectedRoute>} />
|
||||
<Route path="/board/:boardId" element={<ProtectedRoute><Editor /></ProtectedRoute>} />
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuth } from '../auth';
|
||||
import api from '../api';
|
||||
|
||||
/**
|
||||
* AYON entry point: /b?ticket=…&project=…&task=…
|
||||
* Redeems the single-use ticket issued by the AYON addon, stores the
|
||||
* session token and forwards to the task board. No login form involved.
|
||||
*/
|
||||
export default function AyonEntry() {
|
||||
const [params] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuth();
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const ticket = params.get('ticket');
|
||||
if (!ticket) {
|
||||
setError('No ticket provided. Open RefBoard from the AYON launcher.');
|
||||
return;
|
||||
}
|
||||
|
||||
api.post('/api/auth/ayon/exchange', { ticket })
|
||||
.then((res) => {
|
||||
if (res.data?.token && res.data?.user) {
|
||||
login(res.data.token, res.data.user);
|
||||
navigate(res.data.board_url || '/', { replace: true });
|
||||
} else {
|
||||
setError('Unexpected response from server.');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err?.response?.data?.error || 'Ticket exchange failed.');
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
height: '100vh', background: '#1a1a1a', color: '#e0e0e0', gap: '12px',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
{error ? (
|
||||
<>
|
||||
<div style={{ fontSize: '18px' }}>{error}</div>
|
||||
<a href="/login" style={{ color: '#8ab4f8' }}>Go to login</a>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ fontSize: '18px' }}>Signing you in via AYON…</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user