3 Commits

5 changed files with 134 additions and 37 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "challenge-admin-pl", "name": "challenge-admin-pl",
"version": "1.5.1", "version": "1.5.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "challenge-admin-pl", "name": "challenge-admin-pl",
"version": "1.5.1", "version": "1.5.2",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@brojs/cli": "^1.9.4", "@brojs/cli": "^1.9.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "challenge-admin", "name": "challenge-admin",
"version": "1.5.1", "version": "1.5.2",
"description": "", "description": "",
"main": "./src/index.tsx", "main": "./src/index.tsx",
"scripts": { "scripts": {
+23 -3
View File
@@ -31,15 +31,35 @@ export const api = createApi({
) => { ) => {
const response = await fetch(input, init) const response = await fetch(input, init)
if (response.status === 403) keycloak.login() if (response.status === 401 || response.status === 403) {
const { isAuthLoopBlocked, recordAuthAttempt } = await import('../../utils/authLoopGuard')
if (!isAuthLoopBlocked()) {
recordAuthAttempt()
keycloak.login()
} else {
console.error('Auth loop detected, not redirecting to login')
}
}
return response return response
}, },
headers: { headers: {
'Content-Type': 'application/json;charset=utf-8', 'Content-Type': 'application/json;charset=utf-8',
}, },
prepareHeaders: (headers) => { prepareHeaders: async (headers) => {
headers.set('Authorization', `Bearer ${keycloak.token}`) try {
// Обновить токен, если он истекает в течение 30 секунд
await keycloak.updateToken(30)
} catch (error) {
console.error('Failed to refresh token:', error)
}
if (keycloak.token) {
headers.set('Authorization', `Bearer ${keycloak.token}`)
}
return headers
}, },
}), }),
tagTypes: ['Task', 'Chain', 'User', 'Submission', 'Stats'], tagTypes: ['Task', 'Chain', 'User', 'Submission', 'Stats'],
+14 -1
View File
@@ -34,9 +34,22 @@ export const mount = async (Component, element = document.getElementById('app'))
recordAuthAttempt() recordAuthAttempt()
await keycloak.init({ await keycloak.init({
onLoad: 'login-required' onLoad: 'login-required',
checkLoginIframe: false
}) })
// Настройка автоматического обновления токена
setInterval(() => {
keycloak.updateToken(70).then((refreshed) => {
if (refreshed) {
console.log('Token was successfully refreshed')
}
}).catch(() => {
console.error('Failed to refresh token, redirecting to login')
keycloak.login()
})
}, 60000) // Проверять каждую минуту
const userInfo = await keycloak.loadUserInfo() const userInfo = await keycloak.loadUserInfo()
if (userInfo && keycloak.tokenParsed) { if (userInfo && keycloak.tokenParsed) {
+94 -30
View File
@@ -26,6 +26,20 @@ import { LoadingSpinner } from '../../components/LoadingSpinner'
import { ErrorAlert } from '../../components/ErrorAlert' import { ErrorAlert } from '../../components/ErrorAlert'
import { toaster } from '../../components/ui/toaster' import { toaster } from '../../components/ui/toaster'
// Функция для разбиения текста на блоки по 30 строк
const splitTextIntoBlocks = (text: string, linesPerBlock: number = 30): string[] => {
if (!text) return []
const lines = text.split('\n')
const blocks: string[] = []
for (let i = 0; i < lines.length; i += linesPerBlock) {
const block = lines.slice(i, i + linesPerBlock).join('\n')
blocks.push(block)
}
return blocks
}
export const TaskFormPage: React.FC = () => { export const TaskFormPage: React.FC = () => {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const navigate = useNavigate() const navigate = useNavigate()
@@ -394,21 +408,46 @@ export const TaskFormPage: React.FC = () => {
overflowY="auto" overflowY="auto"
> >
{learningMaterial ? ( {learningMaterial ? (
<Box <VStack align="stretch" gap={4}>
className="markdown-preview" {splitTextIntoBlocks(learningMaterial, 30).map((block, index) => (
css={{ <Box
'& a': { key={index}
color: '#0f766e', p={4}
textDecoration: 'underline', borderWidth="2px"
cursor: 'pointer', borderColor="teal.200"
'&:hover': { borderRadius="md"
color: '#115e59', bg="white"
} position="relative"
} className="markdown-preview"
}} css={{
> '& a': {
<ReactMarkdown>{learningMaterial}</ReactMarkdown> color: '#0f766e',
</Box> textDecoration: 'underline',
cursor: 'pointer',
'&:hover': {
color: '#115e59',
}
}
}}
>
<Box
position="absolute"
top="-10px"
left="10px"
bg="teal.500"
color="white"
px={2}
py={1}
borderRadius="md"
fontSize="xs"
fontWeight="bold"
>
Блок {index + 1} (30 строк)
</Box>
<ReactMarkdown>{block}</ReactMarkdown>
</Box>
))}
</VStack>
) : ( ) : (
<Text color="gray.400" fontStyle="italic"> <Text color="gray.400" fontStyle="italic">
{t('challenge.admin.tasks.preview.empty')} {t('challenge.admin.tasks.preview.empty')}
@@ -455,21 +494,46 @@ export const TaskFormPage: React.FC = () => {
height="100%" height="100%"
> >
{learningMaterial ? ( {learningMaterial ? (
<Box <VStack align="stretch" gap={4}>
className="markdown-preview" {splitTextIntoBlocks(learningMaterial, 30).map((block, index) => (
css={{ <Box
'& a': { key={index}
color: '#0f766e', p={4}
textDecoration: 'underline', borderWidth="2px"
cursor: 'pointer', borderColor="teal.200"
'&:hover': { borderRadius="md"
color: '#115e59', bg="white"
} position="relative"
} className="markdown-preview"
}} css={{
> '& a': {
<ReactMarkdown>{learningMaterial}</ReactMarkdown> color: '#0f766e',
</Box> textDecoration: 'underline',
cursor: 'pointer',
'&:hover': {
color: '#115e59',
}
}
}}
>
<Box
position="absolute"
top="-10px"
left="10px"
bg="teal.500"
color="white"
px={2}
py={1}
borderRadius="md"
fontSize="xs"
fontWeight="bold"
>
Блок {index + 1} (30 строк)
</Box>
<ReactMarkdown>{block}</ReactMarkdown>
</Box>
))}
</VStack>
) : ( ) : (
<Text color="gray.400" fontStyle="italic"> <Text color="gray.400" fontStyle="italic">
{t('challenge.admin.tasks.preview.empty')} {t('challenge.admin.tasks.preview.empty')}