Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,25 @@ export default function EditorFileStructure() {
[getTab, removeTabAndSelectFallback],
)

const configurationsRootPath = useMemo(() => {
const paths = project?.filepaths
if (!paths?.length) return

const segments = paths.map((path) => path.replaceAll('\\', '/').split('/').slice(0, -1))

const common = segments.reduce((a, b) => {
let i = 0
while (i < a.length && i < b.length && a[i] === b[i]) i++
return a.slice(0, i)
})

return common.length > 0 ? common.join('/') : undefined
}, [project?.filepaths])

const editorContextMenu = useFileTreeContextMenu({
projectName: project?.name,
dataProvider,
configurationsRootPath,
onAfterRename,
onAfterDelete,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { useCallback, useRef, useState } from 'react'
import { useNavigate } from 'react-router'
import type { TreeItemIndex } from 'react-complex-tree'
import { createFile, deleteFile, renameFile } from '~/services/file-service'
import { createFolderInProject } from '~/services/file-tree-service'
import { clearConfigurationFileCache } from '~/services/configuration-file-service'
import { clearConfigurationFileCache, createConfigurationFile } from '~/services/configuration-file-service'
import useTabStore from '~/stores/tab-store'
import useEditorTabStore from '~/stores/editor-tab-store'
import { showErrorToast } from '~/components/toast'
import { FILE_NAME_PATTERNS, FOLDER_OR_ADAPTER_NAME_PATTERNS } from '~/components/file-structure/name-input-dialog'
import { logApiError } from '~/utils/logger'
import { openInEditor } from '~/actions/navigationActions'

export interface ContextMenuState {
position: { x: number; y: number }
Expand Down Expand Up @@ -43,6 +45,7 @@ export interface DataProviderLike {
interface UseFileTreeContextMenuOptions {
projectName: string | undefined
dataProvider: DataProviderLike | null
configurationsRootPath?: string
onAfterRename?: (oldPath: string, newName: string) => void
onAfterDelete?: (path: string) => void
}
Expand Down Expand Up @@ -70,9 +73,11 @@ function buildNewPath(oldPath: string, newName: string): string {
export function useFileTreeContextMenu({
projectName,
dataProvider,
configurationsRootPath,
onAfterRename,
onAfterDelete,
}: UseFileTreeContextMenuOptions) {
const navigate = useNavigate()
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null)
const [nameDialog, setNameDialog] = useState<NameDialogState | null>(null)
const [deleteTarget, setDeleteTarget] = useState<DeleteTargetState | null>(null)
Expand Down Expand Up @@ -127,9 +132,20 @@ export function useFileTreeContextMenu({
return
}

const filePath = `${parentPath}/${name}`
const isXml = name.toLowerCase().endsWith('.xml')
const configsRoot = configurationsRootPath?.replaceAll('\\', '/')
const normalizedParent = parentPath.replaceAll('\\', '/')
const isInsideConfigurations = !!configsRoot && normalizedParent.startsWith(configsRoot)

try {
await createFile(projectName, `${parentPath}/${name}`)
await (isXml && isInsideConfigurations
? createConfigurationFile(projectName, filePath)
: createFile(projectName, filePath))

await dataProvider.reloadDirectory(parentItemId)

openInEditor(name, filePath, navigate)
} catch (error) {
logApiError('Failed to create file', error as Error)
}
Expand All @@ -138,7 +154,7 @@ export function useFileTreeContextMenu({
patterns: FILE_NAME_PATTERNS,
})
},
[projectName, dataProvider, closeContextMenu],
[projectName, dataProvider, configurationsRootPath, navigate, closeContextMenu],
)

const handleNewFolder = useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useCallback, useRef, useState } from 'react'
import { useNavigate } from 'react-router'
import type { TreeItemIndex } from 'react-complex-tree'
import { deleteFile, renameFile } from '~/services/file-service'
import { createFolderInProject } from '~/services/file-tree-service'
Expand All @@ -12,6 +13,8 @@ import {
FILE_NAME_PATTERNS,
FOLDER_OR_ADAPTER_NAME_PATTERNS,
} from '~/components/file-structure/name-input-dialog'
import { openInStudio } from '~/actions/navigationActions'
import { findAdaptersInXml } from '~/routes/editor/xml-utils'

export type StudioItemType = 'root' | 'folder' | 'configuration' | 'adapter' | 'file'

Expand Down Expand Up @@ -123,6 +126,7 @@ function getRenamePatterns(itemType: StudioItemType): Record<string, RegExp> {
}

export function useStudioContextMenu({ projectName, dataProvider }: UseStudioContextMenuOptions) {
const navigate = useNavigate()
const [contextMenu, setContextMenu] = useState<StudioContextMenuState | null>(null)
const [nameDialog, setNameDialog] = useState<NameDialogState | null>(null)
const [deleteTarget, setDeleteTarget] = useState<DeleteTargetState | null>(null)
Expand Down Expand Up @@ -176,14 +180,12 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
onSubmit: async (name: string) => {
const fileName = ensureXmlExtension(name)
try {
const rootPath = dataProvider.getRootPath().replace(/[/\\]$/, '')
const folderPath = menu.folderPath.replace(/[/\\]$/, '')
const relativePath =
folderPath === rootPath
? fileName
: `${folderPath.slice(rootPath.length + 1).replaceAll('\\', '/')}/${fileName}`
await createConfigurationFile(projectName, relativePath)
const absoluteFilePath = `${folderPath}/${fileName}`
await createConfigurationFile(projectName, absoluteFilePath)
await dataProvider.reloadDirectory('root')

openInStudio(navigate, { adapterName: 'SampleAdapter', filepath: absoluteFilePath, adapterPosition: 0 })
} catch (error) {
logApiError('Failed to create configuration', error as Error)
}
Expand All @@ -192,7 +194,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
patterns: CONFIGURATION_NAME_PATTERNS,
})
},
[projectName, dataProvider, closeContextMenu],
[projectName, dataProvider, navigate, closeContextMenu],
)

const handleNewAdapter = useCallback(
Expand All @@ -206,8 +208,14 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
submitLabel: 'Create',
onSubmit: async (name: string) => {
try {
await createAdapter(projectName, name, menu.path)
const { xmlContent } = await createAdapter(projectName, name, menu.path)
await dataProvider.reloadDirectory('root')

const adapterIndex = findAdaptersInXml(xmlContent).findIndex((adapter) => adapter.name === name)

if (adapterIndex !== -1) {
openInStudio(navigate, { adapterName: name, filepath: menu.path, adapterPosition: adapterIndex })
}
} catch (error) {
logApiError('Failed to create adapter', error as Error)
}
Expand All @@ -216,7 +224,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
patterns: FOLDER_OR_ADAPTER_NAME_PATTERNS,
})
},
[projectName, dataProvider, closeContextMenu],
[projectName, dataProvider, navigate, closeContextMenu],
)

const handleNewFolder = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export default function AddConfigurationModal({
configname = `${configname}.xml`
}

await createConfigurationFile(currentConfiguration.name, `${rootLocationName}/${configname}`)
const folderPath = rootLocationName.replace(/[/\\]$/, '')
const absoluteFilePath = `${folderPath}/${configname}`
await createConfigurationFile(currentConfiguration.name, absoluteFilePath)
const updatedProject = await fetchProject(currentConfiguration.name)
setProject(updatedProject)
onSuccess?.()
Expand Down
4 changes: 2 additions & 2 deletions src/main/frontend/app/services/adapter-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export async function createAdapter(
projectName: string,
adapterName: string,
configurationPath: string,
): Promise<void> {
await apiFetch<void>(`/projects/${encodeURIComponent(projectName)}/adapters`, {
): Promise<XmlResponse> {
return apiFetch<XmlResponse>(`/projects/${encodeURIComponent(projectName)}/adapters`, {
method: 'POST',
body: JSON.stringify({ adapterName, configurationPath }),
})
Expand Down
4 changes: 2 additions & 2 deletions src/main/frontend/app/services/configuration-file-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export async function saveConfigurationFile(
})
}

export async function createConfigurationFile(projectName: string, filename: string): Promise<XmlResponse> {
return apiFetch<XmlResponse>(`${getBaseUrl(projectName)}?name=${encodeURIComponent(filename)}`, { method: 'POST' })
export async function createConfigurationFile(projectName: string, filepath: string): Promise<XmlResponse> {
return apiFetch<XmlResponse>(`${getBaseUrl(projectName)}?path=${encodeURIComponent(filepath)}`, { method: 'POST' })
}

function getBaseUrl(projectName: string): string {
Expand Down
4 changes: 1 addition & 3 deletions src/main/frontend/app/utils/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import variables from '../../environment/environment'

export function apiUrl(path: string): string {
return `${variables.apiBaseUrl}/api${path}`
return `/api${path}`
}

const getAnonymousSessionId = () => {
Expand Down
4 changes: 2 additions & 2 deletions src/main/frontend/app/utils/path-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
export function toRelativePath(absolutePath: string, marker: string): string | null {
const normalizedPath = normalizePath(absolutePath)
const normalizedMarker = normalizePath(marker)
const idx = normalizedPath.indexOf(marker)
return idx === -1 ? null : normalizedMarker.slice(idx + marker.length)
const idx = normalizedPath.indexOf(normalizedMarker)
return idx === -1 ? null : normalizedPath.slice(idx + normalizedMarker.length)
}

export function normalizePath(path: string) {
Expand Down
9 changes: 0 additions & 9 deletions src/main/frontend/environment/base.ts

This file was deleted.

8 changes: 0 additions & 8 deletions src/main/frontend/environment/development.ts

This file was deleted.

9 changes: 0 additions & 9 deletions src/main/frontend/environment/environment.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/main/frontend/environment/production.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/main/frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@ export default defineConfig({
},
server: {
port: 3000,
proxy: {
'/api': 'http://localhost:8080',
},
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ public ResponseEntity<Void> updateAdapter(@RequestBody AdapterUpdateDTO dto) thr
}

@PostMapping("/{projectName}/adapters")
public ResponseEntity<Void> createAdapter(@PathVariable String projectName, @RequestBody AdapterCreateDTO dto) throws IOException {
adapterService.createAdapter(dto.configurationPath(), dto.adapterName());
return ResponseEntity.ok().build();
public ResponseEntity<ConfigurationXmlDTO> createAdapter(@PathVariable String projectName, @RequestBody AdapterCreateDTO dto) throws IOException {
String content = adapterService.createAdapter(dto.configurationPath(), dto.adapterName());
return ResponseEntity.ok(new ConfigurationXmlDTO(content));
}

@PatchMapping("/{projectName}/adapters/rename")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ public ConfigurationXmlDTO getAdapter(String projectName, String configurationPa
throws IOException, ApiException, SAXException, ParserConfigurationException, TransformerException {

ConfigurationProject configurationProject = configurationProjectService.getProject(projectName);
String normalizedConfigPath = configurationPath.replace("\\", "/");
ConfigurationFile config = configurationProject.getConfigurationFiles().stream()
.filter(configurationFile -> configurationFile.getFilepath().equals(configurationPath))
.filter(configurationFile -> configurationFile.getFilepath().replace("\\", "/").equals(normalizedConfigPath))
.findFirst()
.orElseThrow(() -> new ApiException(String.format("Configuration File with path: %s not found", configurationPath), HttpStatus.NOT_FOUND));

Expand Down Expand Up @@ -84,7 +85,7 @@ public boolean updateAdapter(Path configurationFile, String adapterName, String
}
}

public void createAdapter(String configurationPath, String adapterName) throws IOException {
public String createAdapter(String configurationPath, String adapterName) throws IOException {
if (configurationPath == null || configurationPath.isBlank()) {
throw new IllegalArgumentException("Configuration path must not be empty");
}
Expand All @@ -107,6 +108,7 @@ public void createAdapter(String configurationPath, String adapterName) throws I

String updatedXml = XmlConfigurationUtils.convertNodeToString(configDoc);
Files.writeString(absConfigFile, updatedXml, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING);
return updatedXml;
} catch (Exception exception) {
throw new IOException("Failed to create adapter: " + exception.getMessage(), exception);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.frankframework.flow.common.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.jspecify.annotations.NonNull;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;

/**
* Forces the deferred {@link CsrfToken} to load on every request so the XSRF-TOKEN cookie is
* written. Otherwise the cookie is only minted when something reads the token, leaving a SPA with no
* token to echo back as X-XSRF-TOKEN.
*/
public class CsrfCookieFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrfToken != null) {
csrfToken.getToken();
}

filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;

@Configuration
Expand All @@ -49,14 +51,7 @@ public void setEnvironment(Environment environment) {
public SecurityFilterChain configureChain(IAuthenticator authenticator, HttpSecurity http) throws Exception {
configureAuthenticator(authenticator);
http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));
http.csrf(csrf -> {
if (csrfEnabled) {
csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
csrf.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler());
return;
}
csrf.disable();
});
configureCsrf(http);
http.securityMatcher(AnyRequestMatcher.INSTANCE);
http.formLogin(FormLoginConfigurer::disable);
http.logout(LogoutConfigurer::disable);
Expand All @@ -75,4 +70,18 @@ private void configureAuthenticator(IAuthenticator authenticator) {
servletConfig.setUrlMapping("/*");
authenticator.registerServlet(servletConfig);
}

private void configureCsrf(HttpSecurity http) {
if (!csrfEnabled) {
http.csrf(AbstractHttpConfigurer::disable);
return;
}

http.csrf(csrf -> {
csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
csrf.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler());
});

http.addFilterAfter(new CsrfCookieFilter(), CsrfFilter.class);
}
}
Loading
Loading