diff --git a/.gitignore b/.gitignore index 2e1b56cd5..81c0e6c82 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ client/node_modules/ client/packages/lowcoder-plugin-demo/.yarn/install-state.gz client/packages/lowcoder-plugin-demo/yarn.lock client/packages/lowcoder-plugin-demo/.yarn/cache/@types-node-npm-16.18.68-56f72825c0-094ae9ed80.zip +application-dev.yml diff --git a/client/config/test/jest.config.js b/client/config/test/jest.config.js index 90fb74572..53aa19f78 100644 --- a/client/config/test/jest.config.js +++ b/client/config/test/jest.config.js @@ -6,14 +6,10 @@ export function currentDirName(importMetaUrl) { return dirname(fileURLToPath(importMetaUrl)); } - const globals = {}; buildVars.forEach(({ name, defaultValue }) => { globals[name] = process.env[name] || defaultValue; }); -const edition = process.env.REACT_APP_EDITION; -const isEEGlobal = edition === "enterprise-global"; -const isEE = edition === "enterprise" || isEEGlobal; const currentDir = currentDirName(import.meta.url); export default { @@ -22,8 +18,7 @@ export default { "react-markdown": path.resolve(currentDir, "./mocks/react-markdown.js"), "\\.md\\?url$": path.resolve(currentDir, "./mocks/markdown-url-module.js"), "^@lowcoder-ee(.*)$": path.resolve( - currentDir, - isEE ? "../../packages/lowcoder/src/ee/$1" : "../../packages/lowcoder/src/$1" + currentDir, "../../packages/lowcoder/src/$1" ), "lowcoder-sdk": path.resolve(currentDir, "../../packages/lowcoder/src/index.sdk"), }, diff --git a/client/package.json b/client/package.json index 0034bb1d7..e94b3dc4a 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,6 @@ "start": "yarn workspace lowcoder start", "start-win": "LOWCODER_API_SERVICE_URL=http://localhost:3000 yarn start", "start:ee": "REACT_APP_EDITION=enterprise yarn workspace lowcoder start", - "start:ee-global": "REACT_APP_EDITION=enterprise-global yarn workspace lowcoder start", "build": "yarn node ./scripts/build.js", "test": "jest && yarn workspace lowcoder-comps test", "prepare": "yarn workspace lowcoder prepare", diff --git a/client/packages/lowcoder-comps/package.json b/client/packages/lowcoder-comps/package.json index 01733833e..029be11e2 100644 --- a/client/packages/lowcoder-comps/package.json +++ b/client/packages/lowcoder-comps/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-comps", - "version": "0.0.26", + "version": "0.0.27", "type": "module", "license": "MIT", "dependencies": { diff --git a/client/packages/lowcoder-design/src/components/Label.tsx b/client/packages/lowcoder-design/src/components/Label.tsx index 0ec5cce40..91b846acb 100644 --- a/client/packages/lowcoder-design/src/components/Label.tsx +++ b/client/packages/lowcoder-design/src/components/Label.tsx @@ -5,7 +5,6 @@ export const labelCss: any = css` user-select: none; font-size: 13px; - color: #222222; &:hover { cursor: default; diff --git a/client/packages/lowcoder-design/src/components/Section.tsx b/client/packages/lowcoder-design/src/components/Section.tsx index 869c0eabf..7f0ab1738 100644 --- a/client/packages/lowcoder-design/src/components/Section.tsx +++ b/client/packages/lowcoder-design/src/components/Section.tsx @@ -75,6 +75,7 @@ const ShowChildren = styled.div<{ $show?: string; $noMargin?: boolean }>` interface ISectionConfig { name?: string; + open?: boolean; width?: number; noMargin?: boolean; style?: React.CSSProperties; @@ -103,7 +104,9 @@ export const PropertySectionContext = React.createContext) => { const { name } = props; const { compName, state, toggle } = useContext(PropertySectionContext); - const open = name ? state[compName]?.[name] !== false : true; + const open = props.open !== undefined ? props.open : name ? state[compName]?.[name] !== false : true; + + console.log("open", open, props.open); const handleToggle = () => { if (!name) { @@ -142,6 +145,7 @@ export const sectionNames = { validation: trans("prop.validation"), layout: trans("prop.layout"), style: trans("prop.style"), + labelStyle:trans("prop.labelStyle"), data: trans("prop.data"), meetings : trans("prop.meetings"), // added by Falk Wolsky }; diff --git a/client/packages/lowcoder-design/src/i18n/design/locales/en.ts b/client/packages/lowcoder-design/src/i18n/design/locales/en.ts index c6fa81f69..543bb813a 100644 --- a/client/packages/lowcoder-design/src/i18n/design/locales/en.ts +++ b/client/packages/lowcoder-design/src/i18n/design/locales/en.ts @@ -22,6 +22,7 @@ export const en = { advanced: "Advanced", validation: "Validation", layout: "Layout", + labelStyle:"Label Style", style: "Style", meetings : "Meeting Settings", data: "Data", diff --git a/client/packages/lowcoder-design/src/i18n/design/locales/zh.ts b/client/packages/lowcoder-design/src/i18n/design/locales/zh.ts index 3fe60174b..a3622dca0 100644 --- a/client/packages/lowcoder-design/src/i18n/design/locales/zh.ts +++ b/client/packages/lowcoder-design/src/i18n/design/locales/zh.ts @@ -22,6 +22,7 @@ export const zh = { advanced: "高级", validation: "验证", layout: "布局", + labelStyle:"标签样式", style: "样式", meetings: "会议", data: "数据", diff --git a/client/packages/lowcoder-design/src/icons/index.ts b/client/packages/lowcoder-design/src/icons/index.ts index 71c1df6ab..a3b83bd8c 100644 --- a/client/packages/lowcoder-design/src/icons/index.ts +++ b/client/packages/lowcoder-design/src/icons/index.ts @@ -315,6 +315,7 @@ export { ReactComponent as RecyclerIcon } from "./remix/delete-bin-line.svg"; export { ReactComponent as MarketplaceIcon } from "./icon-application-marketplace.svg"; export { ReactComponent as FavoritesIcon } from "./icon-application-favorites.svg"; export { ReactComponent as HomeSettingIcon } from "./remix/settings-4-line.svg"; +export { ReactComponent as EnterpriseIcon } from "./remix/earth-line.svg"; // new diff --git a/client/packages/lowcoder/src/app.tsx b/client/packages/lowcoder/src/app.tsx index 838a9665c..cf691b3f1 100644 --- a/client/packages/lowcoder/src/app.tsx +++ b/client/packages/lowcoder/src/app.tsx @@ -14,7 +14,6 @@ import { IMPORT_APP_FROM_TEMPLATE_URL, INVITE_LANDING_URL, isAuthUnRequired, - MARKETPLACE_TYPE_URL, MARKETPLACE_URL, ORG_AUTH_LOGIN_URL, ORG_AUTH_REGISTER_URL, @@ -22,7 +21,9 @@ import { SETTING, TRASH_URL, USER_AUTH_URL, + ADMIN_APP_URL, } from "constants/routesURL"; + import React from "react"; import { createRoot } from "react-dom/client"; import { Helmet } from "react-helmet"; @@ -38,7 +39,7 @@ import LazyRoute from "components/LazyRoute"; import AppFromTemplate from "pages/ApplicationV2/AppFromTemplate"; import AppEditor from "pages/editor/AppEditor"; import { getAntdLocale } from "i18n/antdLocale"; -import { CodeEditorTooltipContainer } from "base/codeEditor/codeEditor"; +// import { CodeEditorTooltipContainer } from "base/codeEditor/codeEditor"; import { ProductLoading } from "components/ProductLoading"; import { language, trans } from "i18n"; import { loadComps } from "comps"; @@ -111,21 +112,16 @@ class AppIndex extends React.Component { {{this.props.brandName}} {} - - {isLowCoderDomain && ( - <> - {/* setting Meta Attributes to be able for embedding via iframely */} - - - - - - - {/* embedding analytics of Cleabits */} - - - )} - + {isLowCoderDomain && [ + // Adding Support for iframely to be able to embedd the component explorer in the docu + , + , + , + , + , + // adding Clearbit Support for Analytics + + ]} @@ -160,6 +156,7 @@ class AppIndex extends React.Component { TRASH_URL, SETTING, MARKETPLACE_URL, + ADMIN_APP_URL, ]} // component={ApplicationListPage} component={ApplicationHome} diff --git a/client/packages/lowcoder/src/components/CompName.tsx b/client/packages/lowcoder/src/components/CompName.tsx index d6a92dc46..78900c80e 100644 --- a/client/packages/lowcoder/src/components/CompName.tsx +++ b/client/packages/lowcoder/src/components/CompName.tsx @@ -83,6 +83,7 @@ export const CompName = (props: Iprops) => { const items: EditPopoverItemType[] = []; + // Falk: TODO - Implement upgrade for individual Version functionality const handleUpgrade = async () => { if (upgrading) { return; @@ -112,6 +113,13 @@ export const CompName = (props: Iprops) => { if (compInfo.isRemote) { + items.push({ + text: trans("history.currentVersion") + ": " + compInfo.packageVersion, + onClick: () => { + + }, + }); + items.push({ text: trans("comp.menuUpgradeToLatest"), onClick: () => { diff --git a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx index 059dd3852..ab1a992fa 100644 --- a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx @@ -5,6 +5,7 @@ import { styleControl } from "comps/controls/styleControl"; import { InputLikeStyle, InputLikeStyleType, + LabelStyle, } from "comps/controls/styleControlConstants"; import { NameConfig, @@ -73,6 +74,7 @@ const childrenMap = { viewRef: RefControl, allowClear: BoolControl.DEFAULT_TRUE, style: styleControl(InputLikeStyle), + labelStyle:styleControl(LabelStyle), prefixIcon: IconControl, suffixIcon: IconControl, items: jsonControl(convertAutoCompleteData, autoCompleteDate), @@ -276,8 +278,9 @@ let AutoCompleteCompBase = (function () { ), - // style: props.style, - // ...validateState, + style: props.style, + labelStyle:props.labelStyle, + ...validateState, }); }) .setPropertyViewFn((children) => { @@ -335,6 +338,9 @@ let AutoCompleteCompBase = (function () {
{children.style.getPropertyView()}
+
+ {children.labelStyle.getPropertyView()} +
); }) diff --git a/client/packages/lowcoder/src/comps/comps/containerComp/textContainerComp.tsx b/client/packages/lowcoder/src/comps/comps/containerComp/textContainerComp.tsx new file mode 100644 index 000000000..cd2bb7182 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/containerComp/textContainerComp.tsx @@ -0,0 +1,164 @@ +import { BoolCodeControl, StringControl } from "comps/controls/codeControl"; +import { stringExposingStateControl } from "comps/controls/codeStateControl"; +import { ToDataType } from "comps/generators/multi"; +import { + NameConfigHidden, + withExposingConfigs, +} from "comps/generators/withExposing"; +import { NameGenerator } from "comps/utils/nameGenerator"; +import { hiddenPropertyView } from "comps/utils/propertyUtils"; +import { trans } from "i18n"; +import { CompParams } from "lowcoder-core"; +import { Section, sectionNames } from "lowcoder-design"; +import { oldContainerParamsToNew } from "../containerBase"; +import { toSimpleContainerData } from "../containerBase/simpleContainerComp"; +import { + ContainerChildren, + ContainerCompBuilder, +} from "../triContainerComp/triContainerCompBuilder"; +import { TriContainer } from "../triContainerComp/triFloatTextContainer"; +import { dropdownControl } from "comps/controls/dropdownControl"; +import { withDefault } from "@lowcoder-ee/index.sdk"; + +const typeOptions = [ + { + label: "Markdown", + value: "markdown", + }, + { + label: trans("text"), + value: "text", + }, +] as const; + +const floatOptions = [ + { + label: "None", + value: "none", + }, + { + label: "Right", + value: "right", + }, + { + label: "Left", + value: "left", + }, +] as const; + +export const ContainerBaseComp = (function () { + const childrenMap = { + disabled: BoolCodeControl, + text: stringExposingStateControl( + "text", + trans("textShow.text", { name: "{{currentUser.name}}" }) + ), + type: dropdownControl(typeOptions, "markdown"), + float: dropdownControl(floatOptions, "none"), + width: withDefault(StringControl, "60"), + }; + return new ContainerCompBuilder(childrenMap, (props, dispatch) => { + return ; + }) + .setPropertyViewFn((children) => { + return ( + <> +
+ {children.type.propertyView({ + label: trans("value"), + tooltip: trans("textShow.valueTooltip"), + radioButton: true, + })} + {children.text.propertyView({})} + {children.width.propertyView({ + label: trans("container.flowWidth"), + })} +
+
+ {children.container.getPropertyView()} + {children.float.propertyView({ + label: trans("container.floatType"), + tooltip: trans("textShow.valueTooltip"), + radioButton: true, + })} + + {hiddenPropertyView(children)} +
+
+ {children.container.stylePropertyView()} +
+ + ); + }) + .build(); +})(); + +// Compatible with old data +function convertOldContainerParams(params: CompParams) { + // convert older params to old params + let tempParams = oldContainerParamsToNew(params); + + if (tempParams.value) { + const container = tempParams.value.container; + // old params + if ( + container && + (container.hasOwnProperty("layout") || container.hasOwnProperty("items")) + ) { + const autoHeight = tempParams.value.autoHeight; + return { + ...tempParams, + value: { + container: { + showHeader: false, + body: { 0: { view: container } }, + showBody: true, + showFooter: false, + autoHeight: autoHeight, + }, + }, + }; + } + } + return tempParams; +} + +class ContainerTmpComp extends ContainerBaseComp { + constructor(params: CompParams) { + super(convertOldContainerParams(params)); + } +} + +export const ContainerComp = withExposingConfigs(ContainerTmpComp, [ + NameConfigHidden, +]); + +type ContainerDataType = ToDataType>; + +export function defaultContainerData( + compName: string, + nameGenerator: NameGenerator +): ContainerDataType { + return { + container: { + header: toSimpleContainerData([ + { + item: { + compType: "text", + name: nameGenerator.genItemName("containerTitle"), + comp: { + text: "### " + trans("container.title"), + }, + }, + layoutItem: { + i: "", + h: 5, + w: 24, + x: 0, + y: 0, + }, + }, + ]), + }, + }; +} diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx index ceeb9a257..2977a60e2 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx @@ -20,7 +20,7 @@ import { UICompBuilder, withDefault } from "../../generators"; import { CommonNameConfig, depsConfig, withExposingConfigs } from "../../generators/withExposing"; import { formDataChildren, FormDataPropertyView } from "../formComp/formDataConstants"; import { styleControl } from "comps/controls/styleControl"; -import { DateTimeStyle, DateTimeStyleType } from "comps/controls/styleControlConstants"; +import { DateTimeStyle, DateTimeStyleType, LabelStyle } from "comps/controls/styleControlConstants"; import { withMethodExposing } from "../../generators/withMethodExposing"; import { disabledPropertyView, @@ -72,6 +72,7 @@ const commonChildren = { minuteStep: RangeControl.closed(1, 60, 1), secondStep: RangeControl.closed(1, 60, 1), style: styleControl(DateTimeStyle), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)), suffixIcon: withDefault(IconControl, "/icon:regular/calendar"), ...validationChildren, viewRef: RefControl, @@ -159,12 +160,13 @@ export type DateCompViewProps = Pick< export const datePickerControl = new UICompBuilder(childrenMap, (props) => { let time = dayjs(null); - if(props.value.value !== '') { + if (props.value.value !== '') { time = dayjs(props.value.value, DateParser); } return props.label({ required: props.required, style: props.style, + labelStyle:props.labelStyle, children: ( { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
- {requiredPropertyView(children)} - {dateValidationFields(children)} - {timeValidationFields(children)} - {children.customRule.propertyView({})} -
+ {requiredPropertyView(children)} + {dateValidationFields(children)} + {timeValidationFields(children)} + {children.customRule.propertyView({})} +
{children.onEvent.getPropertyView()} {disabledPropertyView(children)} @@ -234,9 +236,9 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { {children.placeholder.propertyView({ label: trans("date.placeholderText") })}
)} - + {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( - <>
+ <>
{timeFields(children, isMobile)} {children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })}
@@ -244,9 +246,14 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && !isMobile && commonAdvanceSection(children)} {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && ( -
- {children.style.getPropertyView()} -
+ <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); @@ -264,10 +271,10 @@ export const dateRangeControl = (function () { return new UICompBuilder(childrenMap, (props) => { let start = dayjs(null); let end = dayjs(null); - if(props.start.value !== '') { + if (props.start.value !== '') { start = dayjs(props.start.value, DateParser); } - if(props.end.value !== '') { + if (props.end.value !== '') { end = dayjs(props.end.value, DateParser); } @@ -310,12 +317,13 @@ export const dateRangeControl = (function () { return props.label({ required: props.required, style: props.style, + labelStyle:props.labelStyle, children: children, ...(startResult.validateStatus !== "success" ? startResult : endResult.validateStatus !== "success" - ? endResult - : startResult), + ? endResult + : startResult), }); }) .setPropertyViewFn((children) => { @@ -337,11 +345,11 @@ export const dateRangeControl = (function () { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
- {requiredPropertyView(children)} - {dateValidationFields(children)} - {timeValidationFields(children)} - {children.customRule.propertyView({})} -
+ {requiredPropertyView(children)} + {dateValidationFields(children)} + {timeValidationFields(children)} + {children.customRule.propertyView({})} +
{children.onEvent.getPropertyView()} {disabledPropertyView(children)} @@ -358,7 +366,7 @@ export const dateRangeControl = (function () { {children.placeholder.propertyView({ label: trans("date.placeholderText") })}
)} - + {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && ( <>
{timeFields(children, isMobile)} @@ -368,9 +376,14 @@ export const dateRangeControl = (function () { {(useContext(EditorContext).editorModeStatus === "logic" || useContext(EditorContext).editorModeStatus === "both") && commonAdvanceSection(children)} {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && ( -
- {children.style.getPropertyView()} -
+ <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} diff --git a/client/packages/lowcoder/src/comps/comps/gridLayoutComp/canvasView.tsx b/client/packages/lowcoder/src/comps/comps/gridLayoutComp/canvasView.tsx index 545608daa..0f8f04164 100644 --- a/client/packages/lowcoder/src/comps/comps/gridLayoutComp/canvasView.tsx +++ b/client/packages/lowcoder/src/comps/comps/gridLayoutComp/canvasView.tsx @@ -21,6 +21,8 @@ import { CanvasContainerID } from "constants/domLocators"; import { CNRootContainer } from "constants/styleSelectors"; import { ScrollBar } from "lowcoder-design"; +// min-height: 100vh; + const UICompContainer = styled.div<{ $maxWidth?: number; readOnly?: boolean; $bgColor: string }>` height: 100%; margin: 0 auto; @@ -113,7 +115,7 @@ export function CanvasView(props: ContainerBaseProps) { $bgColor={bgColor} >
- {/* */} + - {/* */} +
); diff --git a/client/packages/lowcoder/src/comps/comps/layoutComp/containerBodyChildComp.tsx b/client/packages/lowcoder/src/comps/comps/layoutComp/containerBodyChildComp.tsx new file mode 100644 index 000000000..67b557e5d --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/layoutComp/containerBodyChildComp.tsx @@ -0,0 +1,17 @@ +import { MultiCompBuilder } from "comps/generators"; +import { SimpleContainerComp } from "../containerBase/simpleContainerComp"; + +const children = { + view: SimpleContainerComp, + // FIXME: keep extensible +}; + +export const ContainerBodyChildComp = new MultiCompBuilder(children, (props, dispatch) => { + return { + ...props, + dispatch: dispatch, + }; +}) + // TODO + .setPropertyViewFn(() => <>) + .build(); diff --git a/client/packages/lowcoder/src/comps/comps/layoutComp/layout.tsx b/client/packages/lowcoder/src/comps/comps/layoutComp/layout.tsx new file mode 100644 index 000000000..279178b4c --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/layoutComp/layout.tsx @@ -0,0 +1,201 @@ +import { ContainerStyleType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; +import { EditorContext } from "comps/editorState"; +import { BackgroundColorContext } from "comps/utils/backgroundColorContext"; +import { HintPlaceHolder, ScrollBar } from "lowcoder-design"; +import { ReactNode, useContext } from "react"; +import styled, { css } from "styled-components"; +import { checkIsMobile } from "util/commonUtils"; +import { gridItemCompToGridItems, InnerGrid } from "../containerComp/containerView"; +import { LayoutViewProps } from "./layoutCompBuilder"; + +const getStyle = (style: ContainerStyleType) => { + return css` + border-color: ${style.border}; + border-width: ${style.borderWidth}; + border-radius: ${style.radius}; + overflow: hidden; + padding: ${style.padding}; + ${style.background && `background-color: ${style.background};`} + ${style.backgroundImage && `background-image: ${style.backgroundImage};`} + ${style.backgroundImageRepeat && `background-repeat: ${style.backgroundImageRepeat};`} + ${style.backgroundImageSize && `background-size: ${style.backgroundImageSize};`} + ${style.backgroundImagePosition && `background-position: ${style.backgroundImagePosition};`} + ${style.backgroundImageOrigin && `background-origin: ${style.backgroundImageOrigin};`} + `; +}; + +const Wrapper = styled.div<{ $style: ContainerStyleType }>` + display: flex; + flex-flow: column; + height: 100%; + border: 1px solid #d7d9e0; + border-radius: 4px; + ${(props) => props.$style && getStyle(props.$style)} +`; + +const HeaderInnerGrid = styled(InnerGrid)<{ + $backgroundColor: string + $headerBackgroundImage: string; + $headerBackgroundImageRepeat: string; + $headerBackgroundImageSize: string; + $headerBackgroundImagePosition: string; + $headerBackgroundImageOrigin: string; + }>` + overflow: visible; + ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} + border-radius: 0; + ${(props) => props.$headerBackgroundImage && `background-image: ${props.$headerBackgroundImage};`} + ${(props) => props.$headerBackgroundImageRepeat && `background-repeat: ${props.$headerBackgroundImageRepeat};`} + ${(props) => props.$headerBackgroundImageSize && `background-size: ${props.$headerBackgroundImageSize};`} + ${(props) => props.$headerBackgroundImagePosition && `background-position: ${props.$headerBackgroundImagePosition};`} + ${(props) => props.$headerBackgroundImageOrigin && `background-origin: ${props.$headerBackgroundImageOrigin};`} +`; + +const BodyInnerGrid = styled(InnerGrid)<{ + $showBorder: boolean; + $backgroundColor: string; + $borderColor: string; + $borderWidth: string; + $backgroundImage: string; + $backgroundImageRepeat: string; + $backgroundImageSize: string; + $backgroundImagePosition: string; + $backgroundImageOrigin: string; +}>` + border-top: ${(props) => `${props.$showBorder ? props.$borderWidth : 0} solid ${props.$borderColor}`}; + flex: 1; + ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} + border-radius: 0; + ${(props) => props.$backgroundImage && `background-image: ${props.$backgroundImage};`} + ${(props) => props.$backgroundImageRepeat && `background-repeat: ${props.$backgroundImageRepeat};`} + ${(props) => props.$backgroundImageSize && `background-size: ${props.$backgroundImageSize};`} + ${(props) => props.$backgroundImagePosition && `background-position: ${props.$backgroundImagePosition};`} + ${(props) => props.$backgroundImageOrigin && `background-origin: ${props.$backgroundImageOrigin};`} +`; + +const FooterInnerGrid = styled(InnerGrid)<{ + $showBorder: boolean; + $backgroundColor: string; + $borderColor: string; + $borderWidth: string; + $footerBackgroundImage: string; + $footerBackgroundImageRepeat: string; + $footerBackgroundImageSize: string; + $footerBackgroundImagePosition: string; + $footerBackgroundImageOrigin: string; +}>` + border-top: ${(props) => `${props.$showBorder ? props.$borderWidth : 0} solid ${props.$borderColor}`}; + overflow: visible; + ${(props) => props.$backgroundColor && `background-color: ${props.$backgroundColor};`} + border-radius: 0; + ${(props) => props.$footerBackgroundImage && `background-image: ${props.$footerBackgroundImage};`} + ${(props) => props.$footerBackgroundImageRepeat && `background-repeat: ${props.$footerBackgroundImageRepeat};`} + ${(props) => props.$footerBackgroundImageSize && `background-size: ${props.$footerBackgroundImageSize};`} + ${(props) => props.$footerBackgroundImagePosition && `background-position: ${props.$footerBackgroundImagePosition};`} + ${(props) => props.$footerBackgroundImageOrigin && `background-origin: ${props.$footerBackgroundImageOrigin};`} +`; + +export type LayoutProps = LayoutViewProps & { + hintPlaceholder?: ReactNode; +}; + +export function Layout(props: LayoutProps) { + const { container } = props; + const { showHeader, showFooter } = container; + // When the header and footer are not displayed, the body must be displayed + const showBody = container.showBody || (!showHeader && !showFooter); + const scrollbars = container.scrollbars; + + const { items: headerItems, ...otherHeaderProps } = container.header; + const { items: bodyItems, ...otherBodyProps } = container.body["0"].children.view.getView(); + const { items: footerItems, ...otherFooterProps } = container.footer; + const { + style, + headerStyle, + bodyStyle, + footerStyle, + } = container; + + const editorState = useContext(EditorContext); + const maxWidth = editorState.getAppSettings().maxWidth; + const isMobile = checkIsMobile(maxWidth); + const paddingWidth = isMobile ? 8 : 0; + + return ( +
+ + {showHeader && ( + + + + )} + {showBody && ( + + + + + + )} + {showFooter && ( + + + + )} + +
+ ); +} diff --git a/client/packages/lowcoder/src/comps/comps/layoutComp/layoutComp.tsx b/client/packages/lowcoder/src/comps/comps/layoutComp/layoutComp.tsx new file mode 100644 index 000000000..1072e697e --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/layoutComp/layoutComp.tsx @@ -0,0 +1,169 @@ +import { JSONValue } from "util/jsonTypes"; +import { AutoHeightControl } from "comps/controls/autoHeightControl"; +import { BoolControl } from "comps/controls/boolControl"; +import { styleControl } from "comps/controls/styleControl"; +import { + ContainerStyle, + ContainerHeaderStyle, + ContainerBodyStyle, + ContainerFooterStyle, +} from "comps/controls/styleControlConstants"; +import { MultiCompBuilder, sameTypeMap, withDefault } from "comps/generators"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { NameGenerator } from "comps/utils"; +import { fromRecord, Node } from "lowcoder-core"; +import { nodeIsRecord } from "lowcoder-core"; +import _ from "lodash"; +import { ReactNode } from "react"; +import { lastValueIfEqual } from "util/objectUtils"; +import { + CompTree, + fixOldStyleData, + IContainer, + mergeCompTrees, + traverseCompTree, +} from "../containerBase"; +import { SimpleContainerComp } from "../containerBase/simpleContainerComp"; +import { ContainerBodyChildComp } from "./containerBodyChildComp"; +import { trans } from "i18n"; +import { ControlNode } from "lowcoder-design"; + +const childrenMap = { + header: SimpleContainerComp, + // Support future tab or step container expansion + body: withDefault(sameTypeMap(ContainerBodyChildComp), { + 0: { view: { layout: {}, items: {} } }, + }), + footer: SimpleContainerComp, + showHeader: BoolControl.DEFAULT_TRUE, + showBody: BoolControl.DEFAULT_TRUE, + showFooter: BoolControl, + autoHeight: AutoHeightControl, + scrollbars: withDefault(BoolControl, false), + style: styleControl(ContainerStyle), + headerStyle: styleControl(ContainerHeaderStyle), + bodyStyle: styleControl(ContainerBodyStyle), + footerStyle: styleControl(ContainerFooterStyle), +}; + +// Compatible with old style data 2022-8-15 +const LayoutBaseComp = migrateOldData( + new MultiCompBuilder(childrenMap, (props, dispatch) => { + return { ...props, dispatch }; + }).build(), + fixOldStyleData +); + +export class LayoutComp extends LayoutBaseComp implements IContainer { + scrollbars: any; + private allContainers() { + return [ + this.children.header, + ...Object.values(this.children.body.getView()).map((c) => c.children.view), + this.children.footer, + ]; + } + realSimpleContainer(key?: string): SimpleContainerComp | undefined { + // FIXME: When the tab or step container supports header, footer, modify it to the current tab + if (_.isNil(key)) return this.children.body.getView()["0"].children.view; + return this.allContainers().find((container) => container.realSimpleContainer(key)); + } + getCompTree(): CompTree { + return mergeCompTrees(this.allContainers().map((c) => c.getCompTree())); + } + findContainer(key: string): IContainer | undefined { + for (const container of this.allContainers()) { + const foundContainer = container.findContainer(key); + if (foundContainer) { + return foundContainer === container ? this : foundContainer; + } + } + return undefined; + } + getPasteValue(nameGenerator: NameGenerator): JSONValue { + return { + ...this.toJsonValue(), + header: this.children.header.getPasteValue(nameGenerator), + body: _.mapValues(this.children.body.getView(), (comp) => { + return { + ...comp.toJsonValue(), + view: comp.children.view.getPasteValue(nameGenerator), + }; + }), + footer: this.children.footer.getPasteValue(nameGenerator), + }; + } + override autoHeight(): boolean { + return this.children.autoHeight.getView(); + } + + exposingNode() { + // The exposingNodes of the container subcomponents are put together + const allNodes: Record> = {}; + traverseCompTree(this.getCompTree(), (item) => { + const comp = item.children.comp; + let node = comp.exposingNode(); + // plus formDataKey + if (nodeIsRecord(node) && !node.children.hasOwnProperty("formDataKey")) { + const formDataKey = comp.children["formDataKey"]; + if (formDataKey) { + node = fromRecord({ ...node.children, formDataKey: formDataKey.exposingNode() }); + } + } + allNodes[item.children.name.getView()] = node; + return true; + }); + return lastValueIfEqual(this, "exposing_node", fromRecord(allNodes), checkEquals); + } + + getPropertyView(): ControlNode { + return [this.areaPropertyView(), this.heightPropertyView()]; + } + + areaPropertyView() { + return [ + this.children.showHeader.propertyView({ label: trans("prop.showHeader") }), + this.children.showBody.propertyView({ label: trans("prop.showBody") }), + this.children.showFooter.propertyView({ label: trans("prop.showFooter") }), + + ]; + } + + heightPropertyView() { + return [ + this.children.autoHeight.getPropertyView(), + (!this.children.autoHeight.getView()) && this.children.scrollbars.propertyView({ label: trans("prop.scrollbar") }) + ]; + } + + stylePropertyView() { + return this.children.style.getPropertyView(); + } + + headerStylePropertyView() { + return this.children.headerStyle.getPropertyView(); + } + + bodyStylePropertyView() { + return this.children.bodyStyle.getPropertyView(); + } + + footerStylePropertyView() { + return this.children.footerStyle.getPropertyView(); + } +} + +function checkEquals(node1: Node, node2: Node): boolean { + if (node1 === node2) { + return true; + } + if (node1 && node2 && nodeIsRecord(node1) && nodeIsRecord(node2)) { + const a = node1.children; + const b = node2.children; + const keys = Object.keys(a); + return ( + keys.length === Object.keys(b).length && keys.every((key) => checkEquals(a[key], b[key])) + ); + } + return false; +} diff --git a/client/packages/lowcoder/src/comps/comps/layoutComp/layoutCompBuilder.tsx b/client/packages/lowcoder/src/comps/comps/layoutComp/layoutCompBuilder.tsx new file mode 100644 index 000000000..676fa7cb5 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/layoutComp/layoutCompBuilder.tsx @@ -0,0 +1,88 @@ +import { JSONValue } from "util/jsonTypes"; +import { Comp } from "lowcoder-core"; +import { UICompBuilder } from "comps/generators"; +import { + PropertyViewFnTypeForComp, + ToConstructor, + ToViewReturn, + ViewFnTypeForComp, +} from "comps/generators/multi"; +import { NewChildren as UiChildren } from "comps/generators/uiCompBuilder"; +import { NameGenerator } from "comps/utils"; +import { CompTree, IContainer } from "../containerBase"; +import { SimpleContainerComp } from "../containerBase/simpleContainerComp"; +import { LayoutComp } from "./LayoutComp"; +import { ReactNode } from "react"; + +export type ContainerChildren>> = + UiChildren & { + container: InstanceType; + }; + +export function containerChildren>>( + childrenMap: ToConstructor +): ToConstructor> { + return { ...childrenMap, container: LayoutComp } as any; +} + +export type LayoutViewProps = ToViewReturn>; + +export class ContainerCompBuilder< + ViewReturn, + ChildrenCompMap extends Record> +> { + private childrenMap: ToConstructor; + private viewFn: ViewFnTypeForComp>; + private propertyViewFn?: PropertyViewFnTypeForComp>; + /** + * If viewFn is not placed in the constructor, the type of ViewReturn cannot be inferred + */ + constructor( + childrenMap: ToConstructor, + viewFn: ViewFnTypeForComp> + ) { + this.childrenMap = childrenMap; + this.viewFn = viewFn; + } + setPropertyViewFn(propertyViewFn: PropertyViewFnTypeForComp>) { + this.propertyViewFn = propertyViewFn; + return this; + } + build() { + if (!this.propertyViewFn) { + throw new Error("no propertyViewFn provided"); + } + if (this.childrenMap.hasOwnProperty("container")) { + throw new Error("already has container"); + } + const newChildrenMap = containerChildren(this.childrenMap); + const TmpComp = new UICompBuilder(newChildrenMap, (props, dispatch) => { + return this.viewFn(props as any, dispatch) as ReactNode; + }) + .setPropertyViewFn(this.propertyViewFn as any) + .build(); + class Container extends TmpComp implements IContainer { + realSimpleContainer(key?: string): SimpleContainerComp | undefined { + return this.children.container.realSimpleContainer(key); + } + getCompTree(): CompTree { + return this.children.container.getCompTree(); + } + findContainer(key: string): IContainer | undefined { + const foundContainer = this.children.container.findContainer(key); + if (foundContainer) { + return foundContainer === this.children.container ? this : foundContainer; + } + return undefined; + } + getPasteValue(nameGenerator: NameGenerator): JSONValue { + const containerPasteValue = this.children.container.getPasteValue(nameGenerator); + return { container: containerPasteValue }; + } + override autoHeight(): boolean { + return this.children.container.autoHeight(); + } + } + return Container; + } +} diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx index cb5a7836e..58e0a3544 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx @@ -30,7 +30,7 @@ import { formDataChildren, FormDataPropertyView } from "../formComp/formDataCons import { withMethodExposing, refMethods } from "../../generators/withMethodExposing"; import { RefControl } from "../../controls/refControl"; import { styleControl } from "comps/controls/styleControl"; -import { InputLikeStyle, InputLikeStyleType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; +import { InputLikeStyle, InputLikeStyleType, LabelStyle, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; import { disabledPropertyView, hiddenPropertyView, @@ -52,6 +52,8 @@ import { import { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; const getStyle = (style: InputLikeStyleType) => { return css` @@ -256,6 +258,7 @@ const childrenMap = { onEvent: InputEventHandlerControl, viewRef: RefControl, style: styleControl(InputLikeStyle), + labelStyle:styleControl(LabelStyle), prefixIcon: IconControl, // validation @@ -372,12 +375,13 @@ const CustomInputNumber = (props: RecordConstructorToView) = ); }; -const NumberInputTmpComp = (function () { +let NumberInputTmpComp = (function () { return new UICompBuilder(childrenMap, (props) => { return props.label({ required: props.required, children: , style: props.style, + labelStyle:props.labelStyle, ...validate(props), }); }) @@ -425,15 +429,22 @@ const NumberInputTmpComp = (function () { )} {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && ( + <>
{children.style.getPropertyView()}
+
+ {children.labelStyle.getPropertyView()} +
+ )} )) .build(); })(); +NumberInputTmpComp = migrateOldData(NumberInputTmpComp, fixOldInputCompData); + const NumberInputTmp2Comp = withMethodExposing( NumberInputTmpComp, refMethods([ diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx index bea60397c..7deb69530 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx @@ -15,6 +15,7 @@ const RangeSliderBasicComp = (function () { return new UICompBuilder(childrenMap, (props) => { return props.label({ style: props.style, + labelStyle: props.labelStyle, children: ( { @@ -28,7 +29,7 @@ const RangeSliderBasicComp = (function () { range={true} value={[props.start.value, props.end.value]} $style={props.style} - style={{margin: 0}} + style={{ margin: 0 }} onChange={([start, end]) => { props.start.onChange(start); props.end.onChange(end); diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx index b86b07ef6..af9c81b76 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx @@ -19,6 +19,7 @@ const SliderBasicComp = (function () { return new UICompBuilder(childrenMap, (props) => { return props.label({ style: props.style, + labelStyle:props.labelStyle, children: ( { @@ -32,6 +33,7 @@ const SliderBasicComp = (function () { value={props.value.value} $style={props.style} style={{margin: 0}} + // FALK TODO : vertical={true} onChange={(e) => { props.value.onChange(e); props.onEvent("change"); diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx index fd1b84634..04d3b7f48 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx @@ -5,7 +5,7 @@ import { ChangeEventHandlerControl } from "../../controls/eventHandlerControl"; import { Section, sectionNames } from "lowcoder-design"; import { RecordConstructorToComp } from "lowcoder-core"; import { styleControl } from "comps/controls/styleControl"; -import { SliderStyle, SliderStyleType } from "comps/controls/styleControlConstants"; +import { LabelStyle, SliderStyle, SliderStyleType } from "comps/controls/styleControlConstants"; import styled, { css } from "styled-components"; import { default as Slider } from "antd/es/slider"; import { darkenColor, fadeColor } from "lowcoder-design"; @@ -49,6 +49,7 @@ export const SliderStyled = styled(Slider)<{ $style: SliderStyleType }>` ${(props) => props.$style && getStyle(props.$style)} `; +// Falk TODO: height: 300px; export const SliderWrapper = styled.div` width: 100%; display: inline-flex; @@ -67,6 +68,7 @@ export const SliderChildren = { disabled: BoolCodeControl, onEvent: ChangeEventHandlerControl, style: styleControl(SliderStyle), + labelStyle:styleControl(LabelStyle.filter((style)=> ['accent','validate'].includes(style.name) === false)), prefixIcon: IconControl, suffixIcon: IconControl, }; @@ -96,6 +98,9 @@ export const SliderPropertyView = (
{children.style.getPropertyView()}
+
+ {children.labelStyle.getPropertyView()} +
)} diff --git a/client/packages/lowcoder/src/comps/comps/ratingComp.tsx b/client/packages/lowcoder/src/comps/comps/ratingComp.tsx index 40db580ab..df93e688c 100644 --- a/client/packages/lowcoder/src/comps/comps/ratingComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/ratingComp.tsx @@ -10,7 +10,7 @@ import { UICompBuilder, withDefault } from "../generators"; import { CommonNameConfig, NameConfig, withExposingConfigs } from "../generators/withExposing"; import { formDataChildren, FormDataPropertyView } from "./formComp/formDataConstants"; import { styleControl } from "comps/controls/styleControl"; -import { RatingStyle, RatingStyleType } from "comps/controls/styleControlConstants"; +import { LabelStyle, RatingStyle, RatingStyleType } from "comps/controls/styleControlConstants"; import { migrateOldData } from "comps/generators/simpleGenerators"; import { disabledPropertyView, hiddenPropertyView } from "comps/utils/propertyUtils"; import { trans } from "i18n"; @@ -44,6 +44,7 @@ const RatingBasicComp = (function () { disabled: BoolCodeControl, onEvent: eventHandlerControl(EventOptions), style: migrateOldData(styleControl(RatingStyle), fixOldData), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)), ...formDataChildren, }; return new UICompBuilder(childrenMap, (props) => { @@ -64,6 +65,7 @@ const RatingBasicComp = (function () { return props.label({ style: props.style, + labelStyle: props.labelStyle, children: (
- {children.onEvent.getPropertyView()} - {disabledPropertyView(children)} - {hiddenPropertyView(children)} -
+ {children.onEvent.getPropertyView()} + {disabledPropertyView(children)} + {hiddenPropertyView(children)} +
- {children.allowHalf.propertyView({ - label: trans("rating.allowHalf"), - })} + {children.allowHalf.propertyView({ + label: trans("rating.allowHalf"), + })}
)} @@ -110,9 +112,14 @@ const RatingBasicComp = (function () { )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( -
- {children.style.getPropertyView()} -
+ <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); @@ -144,6 +151,6 @@ const getStyle = (style: RatingStyleType) => { `; }; -export const RateStyled = styled(Rate)<{ $style: RatingStyleType }>` +export const RateStyled = styled(Rate) <{ $style: RatingStyleType }>` ${(props) => props.$style && getStyle(props.$style)} `; diff --git a/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx b/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx index 93011ee73..990fd6e16 100644 --- a/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx +++ b/client/packages/lowcoder/src/comps/comps/remoteComp/loaders.tsx @@ -10,13 +10,16 @@ import { async function npmLoader( remoteInfo: RemoteCompInfo ): Promise { - const { packageName, packageVersion = "latest", compName } = remoteInfo; - const entry = `${NPM_PLUGIN_ASSETS_BASE_URL}/${packageName}@${packageVersion}/index.js`; + + // Falk: removed "packageVersion = "latest" as default value fir packageVersion - to ensure no automatic version jumping. + const localPackageVersion = remoteInfo.packageVersion || "latest"; + + const { packageName, packageVersion, compName } = remoteInfo; + const entry = `${NPM_PLUGIN_ASSETS_BASE_URL}/${packageName}@${localPackageVersion}/index.js`; // const entry = `../../../../../public/package/index.js`; // console.log("Entry", entry); try { const module = await import(/* webpackIgnore: true */ entry); - // console.log("Entry 1", module); const comp = module.default?.[compName]; if (!comp) { throw new Error(trans("npm.compNotFound", { compName })); diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx index e89c0e49f..6a5b9babf 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx @@ -21,6 +21,7 @@ let CascaderBasicComp = (function () { return new UICompBuilder(childrenMap, (props) => { return props.label({ style: props.style, + labelStyle:props.labelStyle, children: ( ['accent', 'validate'].includes(style.name) === false)), showSearch: BoolControl.DEFAULT_TRUE, viewRef: RefControl, - margin: MarginControl, + margin: MarginControl, padding: PaddingControl, }; @@ -71,9 +72,14 @@ export const CascaderPropertyView = ( )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( -
- {children.style.getPropertyView()} -
+ <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx index f8b154e42..9ff59edc0 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx @@ -15,13 +15,15 @@ import { } from "./selectInputConstants"; import { formDataChildren } from "../formComp/formDataConstants"; import { styleControl } from "comps/controls/styleControl"; -import { CheckboxStyle, CheckboxStyleType } from "comps/controls/styleControlConstants"; +import { CheckboxStyle, CheckboxStyleType, LabelStyle } from "comps/controls/styleControlConstants"; import { RadioLayoutOptions, RadioPropertyView } from "./radioCompConstants"; import { dropdownControl } from "../../controls/dropdownControl"; import { ValueFromOption } from "lowcoder-design"; import { EllipsisTextCss } from "lowcoder-design"; import { trans } from "i18n"; import { RefControl } from "comps/controls/refControl"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; export const getStyle = (style: CheckboxStyleType) => { return css` @@ -62,13 +64,13 @@ export const getStyle = (style: CheckboxStyleType) => { &:hover .ant-checkbox-inner, .ant-checkbox:hover .ant-checkbox-inner, .ant-checkbox-input + ant-checkbox-inner { - background-color:${style.hoverBackground ? style.hoverBackground :'#fff'}; + background-color:${style.hoverBackground ? style.hoverBackground : '#fff'}; } &:hover .ant-checkbox-checked .ant-checkbox-inner, .ant-checkbox:hover .ant-checkbox-inner, .ant-checkbox-input + ant-checkbox-inner { - background-color:${style.hoverBackground ? style.hoverBackground:'#ffff'}; + background-color:${style.hoverBackground ? style.hoverBackground : '#ffff'}; } &:hover .ant-checkbox-inner, @@ -126,7 +128,7 @@ const CheckboxGroup = styled(AntdCheckboxGroup) <{ }} `; -const CheckboxBasicComp = (function () { +let CheckboxBasicComp = (function () { const childrenMap = { defaultValue: arrayStringExposingStateControl("defaultValue"), value: arrayStringExposingStateControl("value"), @@ -135,6 +137,7 @@ const CheckboxBasicComp = (function () { onEvent: ChangeEventHandlerControl, options: SelectInputOptionControl, style: styleControl(CheckboxStyle), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)), layout: dropdownControl(RadioLayoutOptions, "horizontal"), viewRef: RefControl, @@ -149,6 +152,7 @@ const CheckboxBasicComp = (function () { return props.label({ required: props.required, style: props.style, + labelStyle: props.labelStyle, children: ( { return css` @@ -93,7 +95,7 @@ const Radio = styled(AntdRadioGroup)<{ }} `; -const RadioBasicComp = (function () { +let RadioBasicComp = (function () { return new UICompBuilder(RadioChildrenMap, (props) => { const [ validateState, @@ -102,6 +104,7 @@ const RadioBasicComp = (function () { return props.label({ required: props.required, style: props.style, + labelStyle:props.labelStyle, children: ( , @@ -92,7 +93,10 @@ export const RadioPropertyView = ( )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( + <>
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx index 73a7d4675..a73827c2a 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx @@ -25,6 +25,9 @@ import { RefControl } from "comps/controls/refControl"; import { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; + const getStyle = (style: SegmentStyleType) => { return css` @@ -83,7 +86,7 @@ const SegmentChildrenMap = { ...formDataChildren, }; -const SegmentedControlBasicComp = (function () { +let SegmentedControlBasicComp = (function () { return new UICompBuilder(SegmentChildrenMap, (props) => { const [ validateState, @@ -147,6 +150,8 @@ const SegmentedControlBasicComp = (function () { .build(); })(); +SegmentedControlBasicComp = migrateOldData(SegmentedControlBasicComp, fixOldInputCompData); + export const SegmentedControlComp = withExposingConfigs(SegmentedControlBasicComp, [ new NameConfig("value", trans("selectInput.valueDesc")), SelectInputInvalidConfig, diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx index 1a30f2522..72483ba3b 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx @@ -1,5 +1,5 @@ import { styleControl } from "comps/controls/styleControl"; -import { SelectStyle } from "comps/controls/styleControlConstants"; +import { LabelStyle, SelectStyle } from "comps/controls/styleControlConstants"; import { trans } from "i18n"; import { stringExposingStateControl } from "../../controls/codeStateControl"; import { UICompBuilder } from "../../generators"; @@ -17,13 +17,16 @@ import { } from "./selectInputConstants"; import { useRef } from "react"; import { RecordConstructorToView } from "lowcoder-core"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; +import { migrateOldData } from "comps/generators/simpleGenerators"; -const SelectBasicComp = (function () { +let SelectBasicComp = (function () { const childrenMap = { ...SelectChildrenMap, defaultValue: stringExposingStateControl("defaultValue"), value: stringExposingStateControl("value"), style: styleControl(SelectStyle), + labelStyle: styleControl(LabelStyle) }; return new UICompBuilder(childrenMap, (props, dispatch) => { const [ @@ -35,10 +38,11 @@ const SelectBasicComp = (function () { propsRef.current = props; const valueSet = new Set(props.options.map((o) => o.value)); // Filter illegal default values entered by the user - + return props.label({ required: props.required, style: props.style, + labelStyle:props.labelStyle, children: ( ControlNode }; value: { propertyView: (params: ControlParams) => ControlNode }; style: { getPropertyView: () => ControlNode }; + labelStyle: { getPropertyView: () => ControlNode }; } ) => ( <> @@ -328,10 +329,15 @@ export const SelectPropertyView = ( {["layout", "both"].includes( useContext(EditorContext).editorModeStatus ) && ( -
- {children.style.getPropertyView()} -
- )} + <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ + )} ); diff --git a/client/packages/lowcoder/src/comps/comps/signatureComp.tsx b/client/packages/lowcoder/src/comps/comps/signatureComp.tsx index 8db397915..c36625c1c 100644 --- a/client/packages/lowcoder/src/comps/comps/signatureComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/signatureComp.tsx @@ -8,6 +8,7 @@ import { styleControl } from "comps/controls/styleControl"; import { contrastColor, SignatureStyle, + LabelStyle, SignatureStyleType, widthCalculator, heightCalculator @@ -38,11 +39,11 @@ const Wrapper = styled.div<{ $style: SignatureStyleType; $isEmpty: boolean }>` overflow: hidden; width: 100%; height: 100%; - width: ${(props) => { - return widthCalculator(props.$style.margin); + width: ${(props) => { + return widthCalculator(props.$style.margin); }}; - height: ${(props) => { - return heightCalculator(props.$style.margin); + height: ${(props) => { + return heightCalculator(props.$style.margin); }}; margin: ${(props) => props.$style.margin}; padding: ${(props) => props.$style.padding}; @@ -98,6 +99,7 @@ const childrenMap = { onEvent: ChangeEventHandlerControl, label: withDefault(LabelControl, { position: "column", text: "" }), style: styleControl(SignatureStyle), + labelStyle: styleControl(LabelStyle), showUndo: withDefault(BoolControl, true), showClear: withDefault(BoolControl, true), value: stateComp(""), @@ -126,6 +128,7 @@ let SignatureTmpComp = (function () { }; return props.label({ style: props.style, + labelStyle:props.labelStyle, children: ( { @@ -218,9 +221,14 @@ let SignatureTmpComp = (function () { )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( -
- {children.style.getPropertyView()} -
+ <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/switchComp.tsx b/client/packages/lowcoder/src/comps/comps/switchComp.tsx index 3a3ecf955..70c6a4379 100644 --- a/client/packages/lowcoder/src/comps/comps/switchComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/switchComp.tsx @@ -4,7 +4,7 @@ import { booleanExposingStateControl } from "comps/controls/codeStateControl"; import { changeEvent, eventHandlerControl } from "comps/controls/eventHandlerControl"; import { LabelControl } from "comps/controls/labelControl"; import { styleControl } from "comps/controls/styleControl"; -import { SwitchStyle, SwitchStyleType } from "comps/controls/styleControlConstants"; +import { SwitchStyle, SwitchStyleType, LabelStyle } from "comps/controls/styleControlConstants"; import { migrateOldData } from "comps/generators/simpleGenerators"; import { Section, sectionNames } from "lowcoder-design"; import styled, { css } from "styled-components"; @@ -90,6 +90,7 @@ let SwitchTmpComp = (function () { onEvent: eventHandlerControl(EventOptions), disabled: BoolCodeControl, style: migrateOldData(styleControl(SwitchStyle), fixOldData), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)), viewRef: RefControl, ...formDataChildren, @@ -97,6 +98,7 @@ let SwitchTmpComp = (function () { return new UICompBuilder(childrenMap, (props) => { return props.label({ style: props.style, + labelStyle:props.labelStyle, children: ( - {children.style.getPropertyView()} - + <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx index 4d7c189c4..dbe8f361e 100644 --- a/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx @@ -256,6 +256,7 @@ const TabbedContainer = (props: TabbedContainerProps) => { return (
` +const InputStyle = styled(Input) <{ $style: InputLikeStyleType }>` ${(props) => props.$style && getStyle(props.$style)} `; @@ -48,11 +50,12 @@ const childrenMap = { showCount: BoolControl, allowClear: BoolControl, style: styleControl(InputLikeStyle), + labelStyle: styleControl(LabelStyle), prefixIcon: IconControl, suffixIcon: IconControl, }; -export const InputComp = new UICompBuilder(childrenMap, (props) => { +let InputBasicComp = new UICompBuilder(childrenMap, (props) => { const [inputProps, validateState] = useTextInputProps(props); return props.label({ required: props.required, @@ -68,6 +71,7 @@ export const InputComp = new UICompBuilder(childrenMap, (props) => { /> ), style: props.style, + labelStyle: props.labelStyle, ...validateState, }); }) @@ -80,22 +84,25 @@ export const InputComp = new UICompBuilder(childrenMap, (props) => { {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( children.label.getPropertyView() )} - + {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( <> -
{hiddenPropertyView(children)}
-
- {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} - {children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })} - {children.showCount.propertyView({ label: trans("prop.showCount") })} - {allowClearPropertyView(children)} - {readOnlyPropertyView(children)} -
- +
{hiddenPropertyView(children)}
+
+ {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} + {children.suffixIcon.propertyView({ label: trans("button.suffixIcon") })} + {children.showCount.propertyView({ label: trans("prop.showCount") })} + {allowClearPropertyView(children)} + {readOnlyPropertyView(children)} +
+ )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( - <>
{children.style.getPropertyView()}
+ <> +
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} ); @@ -108,3 +115,8 @@ export const InputComp = new UICompBuilder(childrenMap, (props) => { ...TextInputConfigs, ]) .build(); + + +const InputComp = migrateOldData(InputBasicComp, fixOldInputCompData); + +export { InputComp }; diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx index 51815260f..9bad13d1e 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx @@ -12,6 +12,7 @@ import { UICompBuilder } from "../../generators"; import { FormDataPropertyView } from "../formComp/formDataConstants"; import { checkMentionListData, + fixOldInputCompData, textInputChildren, } from "./textInputConstants"; import { @@ -42,7 +43,7 @@ import { blurMethod, focusWithOptions } from "comps/utils/methodUtils"; import { textInputValidate, } from "../textInputComp/textInputConstants"; -import { jsonControl } from "@lowcoder-ee/comps/controls/codeControl"; +import { jsonControl } from "comps/controls/codeControl"; import { submitEvent, eventHandlerControl, @@ -54,6 +55,7 @@ import { import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; const Wrapper = styled.div<{ $style: InputLikeStyleType; @@ -267,12 +269,15 @@ let MentionTmpComp = (function () { .build(); })(); + MentionTmpComp = class extends MentionTmpComp { override autoHeight(): boolean { return this.children.autoHeight.getView(); } }; +MentionTmpComp = migrateOldData(MentionTmpComp, fixOldInputCompData); + const TextareaTmp2Comp = withMethodExposing( MentionTmpComp, refMethods([focusWithOptions, blurMethod]) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx index b5c3d701d..19944f019 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx @@ -13,6 +13,7 @@ import { LabelControl } from "../../controls/labelControl"; import { UICompBuilder, withDefault } from "../../generators"; import { FormDataPropertyView } from "../formComp/formDataConstants"; import { + fixOldInputCompData, getStyle, inputRefMethods, TextInputBasicSection, @@ -25,7 +26,7 @@ import { import { withMethodExposing } from "../../generators/withMethodExposing"; import { styleControl } from "comps/controls/styleControl"; import styled from "styled-components"; -import { InputLikeStyle, InputLikeStyleType } from "comps/controls/styleControlConstants"; +import { InputLikeStyle, InputLikeStyleType, LabelStyle } from "comps/controls/styleControlConstants"; import { hiddenPropertyView, minLengthPropertyView, @@ -40,14 +41,15 @@ import { hasIcon } from "comps/utils"; import { RefControl } from "comps/controls/refControl"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; -const PasswordStyle = styled(InputPassword)<{ +const PasswordStyle = styled(InputPassword) <{ $style: InputLikeStyleType; }>` ${(props) => props.$style && getStyle(props.$style)} `; -const PasswordTmpComp = (function () { +let PasswordTmpComp = (function () { const childrenMap = { ...textInputChildren, viewRef: RefControl, @@ -56,6 +58,7 @@ const PasswordTmpComp = (function () { visibilityToggle: BoolControl.DEFAULT_TRUE, prefixIcon: IconControl, style: styleControl(InputLikeStyle), + labelStyle: styleControl(LabelStyle) }; return new UICompBuilder(childrenMap, (props) => { const [inputProps, validateState] = useTextInputProps(props); @@ -71,6 +74,7 @@ const PasswordTmpComp = (function () { /> ), style: props.style, + labelStyle:props.labelStyle, ...validateState, }); }) @@ -86,24 +90,27 @@ const PasswordTmpComp = (function () { {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( <> -
{hiddenPropertyView(children)}
-
- {children.visibilityToggle.propertyView({ - label: trans("password.visibilityToggle"), - })} - {readOnlyPropertyView(children)} - {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} -
+
{hiddenPropertyView(children)}
+
+ {children.visibilityToggle.propertyView({ + label: trans("password.visibilityToggle"), + })} + {readOnlyPropertyView(children)} + {children.prefixIcon.propertyView({ label: trans("button.prefixIcon") })} +
{requiredPropertyView(children)} {regexPropertyView(children)} {minLengthPropertyView(children)} {maxLengthPropertyView(children)} {children.customRule.propertyView({})} -
+
)} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( - <>
{children.style.getPropertyView()}
+ <> +
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} ); @@ -111,6 +118,8 @@ const PasswordTmpComp = (function () { .build(); })(); +PasswordTmpComp = migrateOldData(PasswordTmpComp, fixOldInputCompData); + const PasswordTmp2Comp = withMethodExposing(PasswordTmpComp, inputRefMethods); export const PasswordComp = withExposingConfigs(PasswordTmp2Comp, [ diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx index e11027a69..d39ce2a30 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx @@ -10,6 +10,7 @@ import { AutoHeightControl } from "../../controls/autoHeightControl"; import { UICompBuilder, withDefault } from "../../generators"; import { FormDataPropertyView } from "../formComp/formDataConstants"; import { + fixOldInputCompData, getStyle, TextInputBasicSection, textInputChildren, @@ -21,7 +22,7 @@ import { import { withMethodExposing, refMethods } from "../../generators/withMethodExposing"; import { styleControl } from "comps/controls/styleControl"; import styled from "styled-components"; -import { InputLikeStyle, InputLikeStyleType } from "comps/controls/styleControlConstants"; +import { InputLikeStyle, InputLikeStyleType, LabelStyle } from "comps/controls/styleControlConstants"; import { TextArea } from "components/TextArea"; import { allowClearPropertyView, @@ -35,8 +36,9 @@ import { blurMethod, focusWithOptions } from "comps/utils/methodUtils"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; -const TextAreaStyled = styled(TextArea)<{ +const TextAreaStyled = styled(TextArea) <{ $style: InputLikeStyleType; }>` ${(props) => props.$style && getStyle(props.$style)} @@ -46,7 +48,7 @@ const Wrapper = styled.div<{ $style: InputLikeStyleType; }>` height: 100% !important; - + .ant-input { height:100% !important; } @@ -70,6 +72,7 @@ let TextAreaTmpComp = (function () { allowClear: BoolControl, autoHeight: withDefault(AutoHeightControl, "fixed"), style: styleControl(InputLikeStyle), + labelStyle: styleControl(LabelStyle) }; return new UICompBuilder(childrenMap, (props) => { const [inputProps, validateState] = useTextInputProps(props); @@ -77,7 +80,7 @@ let TextAreaTmpComp = (function () { required: props.required, children: ( - ), style: props.style, + labelStyle: props.labelStyle, ...validateState, }); }) @@ -101,19 +105,22 @@ let TextAreaTmpComp = (function () { {["logic", "both"].includes(useContext(EditorContext).editorModeStatus) && ( <> -
- {children.autoHeight.getPropertyView()} - {hiddenPropertyView(children)} -
-
- {allowClearPropertyView(children)} - {readOnlyPropertyView(children)} -
- +
+ {children.autoHeight.getPropertyView()} + {hiddenPropertyView(children)} +
+
+ {allowClearPropertyView(children)} + {readOnlyPropertyView(children)} +
+ )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( - <>
{children.style.getPropertyView()}
+ <> +
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} )) @@ -126,6 +133,8 @@ TextAreaTmpComp = class extends TextAreaTmpComp { } }; +TextAreaTmpComp = migrateOldData(TextAreaTmpComp, fixOldInputCompData); + const TextareaTmp2Comp = withMethodExposing( TextAreaTmpComp, refMethods([focusWithOptions, blurMethod]) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 1d01266af..e0b39b127 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -9,7 +9,7 @@ import { } from "comps/controls/codeControl"; import { stringExposingStateControl } from "comps/controls/codeStateControl"; import { LabelControl } from "comps/controls/labelControl"; -import { InputLikeStyleType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; +import { InputLikeStyleType, LabelStyleType, heightCalculator, widthCalculator } from "comps/controls/styleControlConstants"; import { Section, sectionNames, ValueFromOption } from "lowcoder-design"; import _ from "lodash"; import { css } from "styled-components"; @@ -235,7 +235,7 @@ export const TextInputValidationSection = (children: TextInputComp) => ( ); -export function getStyle(style: InputLikeStyleType) { +export function getStyle(style: InputLikeStyleType, labelStyle?: LabelStyleType) { return css` border-radius: ${style.radius}; border-width: ${style.borderWidth}; @@ -250,7 +250,7 @@ export function getStyle(style: InputLikeStyleType) { font-style:${style.fontStyle}; text-transform:${style.textTransform}; text-decoration:${style.textDecoration}; - background-color: ${style.background}; + // background-color: ${style.background}; border-color: ${style.border}; &:focus, @@ -305,3 +305,17 @@ export function checkMentionListData(data: any) { } return data } + +// separate defaultValue and value for old components +export function fixOldInputCompData(oldData: any) { + if (!oldData) return oldData; + if (Boolean(oldData.value) && !Boolean(oldData.defaultValue)) { + const value = oldData.value; + return { + ...oldData, + defaultValue: value, + value: '', + }; + } + return oldData; +} diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx index 6474efa9b..753df6688 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from "react"; import styled from "styled-components"; import ReactResizeDetector from "react-resize-detector"; import { StyleConfigType, styleControl } from "comps/controls/styleControl"; -import { TreeStyle } from "comps/controls/styleControlConstants"; +import { LabelStyle, TreeStyle } from "comps/controls/styleControlConstants"; import { LabelControl } from "comps/controls/labelControl"; import { withDefault } from "comps/generators"; import { dropdownControl } from "comps/controls/dropdownControl"; @@ -77,10 +77,11 @@ const childrenMap = { // TODO: more event onEvent: SelectEventHandlerControl, style: styleControl(TreeStyle), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)) }; const TreeCompView = (props: RecordConstructorToView) => { - const { treeData, selectType, value, expanded, checkStrictly, style } = props; + const { treeData, selectType, value, expanded, checkStrictly, style, labelStyle } = props; const [height, setHeight] = useState(); const selectable = selectType === "single" || selectType === "multi"; const checkable = selectType === "check"; @@ -95,7 +96,8 @@ const TreeCompView = (props: RecordConstructorToView) => { return props.label({ required: props.required, ...selectInputValidate(props), - style: style, + style, + labelStyle, children: ( setHeight(h)}> @@ -166,7 +168,7 @@ let TreeBasicComp = (function () { )} - + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && (
{children.expanded.propertyView({ label: trans("tree.expanded") })} @@ -176,10 +178,13 @@ let TreeBasicComp = (function () {
)} - {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( children.label.getPropertyView() )} + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && (children.label.getPropertyView())} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( -
{children.style.getPropertyView()}
+ <> +
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} )) diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx index ea31712a3..de15121b3 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx @@ -6,7 +6,7 @@ import { default as TreeSelect } from "antd/es/tree-select"; import { useEffect } from "react"; import styled from "styled-components"; import { styleControl } from "comps/controls/styleControl"; -import { TreeSelectStyle, TreeSelectStyleType } from "comps/controls/styleControlConstants"; +import { LabelStyle, TreeSelectStyle, TreeSelectStyleType } from "comps/controls/styleControlConstants"; import { LabelControl } from "comps/controls/labelControl"; import { dropdownControl } from "comps/controls/dropdownControl"; import { @@ -66,6 +66,7 @@ const childrenMap = { showSearch: BoolControl.DEFAULT_TRUE, inputValue: stateComp(""), // search value style: styleControl(TreeSelectStyle), + labelStyle:styleControl(LabelStyle), viewRef: RefControl, }; @@ -83,7 +84,7 @@ function getCheckedStrategy(v: ValueFromOption) { const TreeCompView = ( props: RecordConstructorToView & { dispatch: DispatchType } ) => { - const { treeData, selectType, value, expanded, style, inputValue } = props; + const { treeData, selectType, value, expanded, style,labelStyle, inputValue } = props; const isSingle = selectType === "single"; const [ validateState, @@ -99,7 +100,8 @@ const TreeCompView = ( return props.label({ required: props.required, ...validateState, - style: style, + style, + labelStyle, children: (
{children.style.getPropertyView()}
+
{children.labelStyle.getPropertyView()}
+ )} - - - )) .setExposeMethodConfigs(baseSelectRefMethods) diff --git a/client/packages/lowcoder/src/comps/comps/triContainerComp/triFloatTextContainer.tsx b/client/packages/lowcoder/src/comps/comps/triContainerComp/triFloatTextContainer.tsx new file mode 100644 index 000000000..dcb5dec4d --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/triContainerComp/triFloatTextContainer.tsx @@ -0,0 +1,180 @@ +import { + ContainerStyleType, + heightCalculator, + widthCalculator, +} from "comps/controls/styleControlConstants"; +import { EditorContext } from "comps/editorState"; +import { BackgroundColorContext } from "comps/utils/backgroundColorContext"; +import { HintPlaceHolder, TacoMarkDown } from "lowcoder-design"; +import { ReactNode, useContext } from "react"; +import styled, { css } from "styled-components"; +import { checkIsMobile } from "util/commonUtils"; +import { + gridItemCompToGridItems, + InnerGrid, +} from "../containerComp/containerView"; +import { TriContainerViewProps } from "../triContainerComp/triContainerCompBuilder"; + +const getStyle = (style: ContainerStyleType) => { + return css` + border-color: ${style.border}; + border-radius: ${style.radius}; + border-width: ${style.borderWidth}; + overflow: hidden; + margin: ${style.margin}; + padding: ${style.padding}; + + width: ${widthCalculator(style.margin)}; + height: ${heightCalculator(style.margin)}; + `; +}; + +const Wrapper = styled.div<{ $style: ContainerStyleType }>` + display: flex; + flex-flow: column; + height: 100%; + border: 1px solid #d7d9e0; + border-radius: 4px; + ${(props) => props.$style && getStyle(props.$style)} +`; + +const HeaderInnerGrid = styled(InnerGrid)<{ backgroundColor: string }>` + overflow: visible; + ${(props) => + props.backgroundColor && `background-color: ${props.backgroundColor};`} + border-radius: 0; +`; + +const BodyInnerGrid = styled(InnerGrid)<{ + showBorder: boolean; + backgroundColor: string; + borderColor: string; +}>` + border-top: ${(props) => + `${props.showBorder ? 1 : 0}px solid ${props.borderColor}`}; + flex: 1; + ${(props) => + props.backgroundColor && `background-color: ${props.backgroundColor};`} + border-radius: 0; +`; + +const FooterInnerGrid = styled(InnerGrid)<{ + showBorder: boolean; + backgroundColor: string; + borderColor: string; +}>` + border-top: ${(props) => + `${props.showBorder ? 1 : 0}px solid ${props.borderColor}`}; + overflow: visible; + ${(props) => + props.backgroundColor && `background-color: ${props.backgroundColor};`} + border-radius: 0; +`; + +export type TriContainerProps = TriContainerViewProps & { + hintPlaceholder?: ReactNode; + text: { + value: string; + }; + type: string; + float: string; + width: string; +}; + +export function TriContainer(props: TriContainerProps) { + const { container, text } = props; + const { showHeader, showFooter } = container; + // When the header and footer are not displayed, the body must be displayed + const showBody = container.showBody || (!showHeader && !showFooter); + + const { items: headerItems, ...otherHeaderProps } = container.header; + const { items: bodyItems, ...otherBodyProps } = + container.body["0"].children.view.getView(); + const { items: footerItems, ...otherFooterProps } = container.footer; + const { style } = container; + + const editorState = useContext(EditorContext); + const maxWidth = editorState.getAppSettings().maxWidth; + const isMobile = checkIsMobile(maxWidth); + const paddingWidth = isMobile ? 7 : 19; + + return ( + + {showHeader && ( + + + + )} + {showBody && ( + +
+ +

+ {props.type === "markdown" ? ( + {text.value} + ) : ( + text.value + )} +

+
+
+ )} + {showFooter && ( + + + + )} +
+ ); +} diff --git a/client/packages/lowcoder/src/comps/controls/labelControl.tsx b/client/packages/lowcoder/src/comps/controls/labelControl.tsx index 74a4f26ac..465bce18c 100644 --- a/client/packages/lowcoder/src/comps/controls/labelControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/labelControl.tsx @@ -9,17 +9,18 @@ import { MultiCompBuilder } from "comps/generators/multi"; import { labelCss, Section, Tooltip, UnderlineCss } from "lowcoder-design"; import { ValueFromOption } from "lowcoder-design"; import { isEmpty } from "lodash"; -import { ReactNode } from "react"; +import { Fragment, ReactNode } from "react"; import styled, { css } from "styled-components"; import { AlignLeft } from "lowcoder-design"; import { AlignRight } from "lowcoder-design"; import { StarIcon } from "lowcoder-design"; -import { heightCalculator, widthCalculator } from "./styleControlConstants"; +import { LabelStyleType, heightCalculator, widthCalculator } from "./styleControlConstants"; type LabelViewProps = Pick & { children: ReactNode; style?: Record; + labelStyle?: Record; }; const StyledStarIcon = styled(StarIcon)` @@ -75,10 +76,22 @@ const LabelWrapper = styled.div<{ max-width: ${(props) => (props.$position === "row" ? "80%" : "100%")}; flex-shrink: 0; `; +// ${(props) => props.$border && UnderlineCss}; +// ${(props) => props.$border && `border-bottom:${props.$labelStyle.borderWidth} ${props.$labelStyle.borderStyle} ${!!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.border};`} -const Label = styled.span<{ $border: boolean }>` +const Label = styled.span<{ $border: boolean, $labelStyle: LabelStyleType, $validateStatus: "success" | "warning" | "error" | "validating" | null }>` ${labelCss}; - ${(props) => props.$border && UnderlineCss}; + font-family:${(props) => props.$labelStyle.fontFamily}; + font-weight:${(props) => props.$labelStyle.textWeight}; + font-style:${(props) => props.$labelStyle.fontStyle}; + text-transform:${(props) => props.$labelStyle.textTransform}; + text-decoration:${(props) => props.$labelStyle.textDecoration}; + font-size:${(props) => props.$labelStyle.textSize}; + color:${(props) => !!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.label} !important; + ${(props) => `border-bottom:${props.$labelStyle.borderWidth} ${props.$labelStyle.borderStyle} ${!!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.border};`} + border-radius:${(props) => props.$labelStyle.radius}; + padding:${(props) => props.$labelStyle.padding}; + margin:${(props) => props.$labelStyle.margin}; width: fit-content; user-select: text; white-space: nowrap; @@ -144,21 +157,22 @@ export const LabelControl = (function () { position: dropdownControl(PositionOptions, "row"), align: dropdownControl(AlignOptions, "left"), }; + return new MultiCompBuilder(childrenMap, (props) => (args: LabelViewProps) => ( - {!props.hidden && !isEmpty(props.text) && ( node.closest(".react-grid-item")} > - + {args.required && } @@ -210,8 +229,8 @@ export const LabelControl = (function () { args.validateStatus === "error" ? red.primary : args.validateStatus === "warning" - ? yellow.primary - : green.primary + ? yellow.primary + : green.primary } > {args.help} diff --git a/client/packages/lowcoder/src/comps/controls/styleControl.tsx b/client/packages/lowcoder/src/comps/controls/styleControl.tsx index ec5663a7e..7648814bb 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControl.tsx @@ -53,6 +53,9 @@ import { FooterBackgroundImageSizeConfig, FooterBackgroundImagePositionConfig, FooterBackgroundImageOriginConfig, + TextTransformConfig, + TextDecorationConfig, + borderStyleConfig, } from "./styleControlConstants"; @@ -140,6 +143,15 @@ function isFontFamilyConfig(config: SingleColorConfig): config is FontFamilyConf function isFontStyleConfig(config: SingleColorConfig): config is FontStyleConfig { return config.hasOwnProperty("fontStyle"); } +function isTextTransformConfig(config: SingleColorConfig): config is TextTransformConfig { + return config.hasOwnProperty("textTransform"); +} +function isTextDecorationConfig(config: SingleColorConfig): config is TextDecorationConfig { + return config.hasOwnProperty("textDecoration"); +} +function isBorderStyleConfig(config: SingleColorConfig): config is borderStyleConfig { + return config.hasOwnProperty("borderStyle"); +} function isMarginConfig(config: SingleColorConfig): config is MarginConfig { return config.hasOwnProperty("margin"); @@ -222,6 +234,15 @@ function isEmptyFontFamily(fontFamily: string) { function isEmptyFontStyle(fontStyle: string) { return _.isEmpty(fontStyle); } +function isEmptyTextTransform(textTransform: string) { + return _.isEmpty(textTransform); +} +function isEmptyTextDecoration(textDecoration: string) { + return _.isEmpty(textDecoration); +} +function isEmptyBorderStyle(borderStyle: string) { + return _.isEmpty(borderStyle); +} function isEmptyMargin(margin: string) { return _.isEmpty(margin); @@ -328,6 +349,18 @@ function calcColors>( res[name] = props[name]; return; } + if (!isEmptyTextTransform(props[name]) && isTextTransformConfig(config)) { + res[name] = props[name]; + return; + } + if (!isEmptyTextDecoration(props[name]) && isTextDecorationConfig(config)) { + res[name] = props[name]; + return; + } + if (!isEmptyBorderStyle(props[name]) && isBorderStyleConfig(config)) { + res[name] = props[name]; + return; + } if (!isEmptyMargin(props[name]) && isMarginConfig(config)) { res[name] = props[name]; return; @@ -412,6 +445,15 @@ function calcColors>( if (isFontStyleConfig(config)) { res[name] = themeWithDefault[config.fontStyle] || 'normal' } + if(isTextTransformConfig(config)){ + res[name] = themeWithDefault[config.textTransform] || 'none' + } + if(isTextDecorationConfig(config)){ + res[name] = themeWithDefault[config.textDecoration] || 'none' + } + if(isBorderStyleConfig(config)){ + res[name] = themeWithDefault[config.borderStyle] || 'dashed' + } if (isMarginConfig(config)) { res[name] = themeWithDefault[config.margin]; } @@ -550,6 +592,7 @@ export function styleControl(colorConfig name === "textTransform" || name === "textDecoration" || name === "fontFamily" || + name === "borderStyle" || name === "fontStyle" || name === "backgroundImage" || name === "backgroundImageRepeat" || @@ -688,24 +731,20 @@ export function styleControl(colorConfig label: config.label, preInputNode: , placeholder: props[name], - }) - : name === "margin" + }) : name === "borderStyle" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : (name === "padding" || - name === "containerheaderpadding" || - name === "containerfooterpadding" || - name === "containerbodypadding") + : name === "margin" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) : name === "textSize" || @@ -717,71 +756,69 @@ export function styleControl(colorConfig children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "textWeight" + : name === "textSize" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "fontFamily" + : name === "textWeight" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "fontStyle" + : name === "fontFamily" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], - }) - : name === "backgroundImage" || name === "headerBackgroundImage" || name === "footerBackgroundImage" + }) : name === "textDecoration" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], - }) - : name === "backgroundImageRepeat" || name === "headerBackgroundImageRepeat" || name === "footerBackgroundImageRepeat" + }) : name === "textTransform" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "backgroundImageSize" || name === "headerBackgroundImageSize" || name === "footerBackgroundImageSize" + : name === "fontStyle" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "backgroundImagePosition" || name === "headerBackgroundImagePosition" || name === "footerBackgroundImagePosition" + : name === "backgroundImage" || name === "headerBackgroundImage" || name === "footerBackgroundImage" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) - : name === "backgroundImageOrigin" || name === "headerBackgroundImageOrigin" || name === "footerBackgroundImageOrigin" + : name === "backgroundImageRepeat" || name === "headerBackgroundImageRepeat" || name === "footerBackgroundImageRepeat" ? ( children[name] as InstanceType ).propertyView({ label: config.label, - preInputNode: , + preInputNode: , placeholder: props[name], }) : children[name].propertyView({ @@ -793,7 +830,6 @@ export function styleControl(colorConfig }) } -
); })} diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 1241ae070..6e4297907 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -63,7 +63,6 @@ export type borderStyleConfig = CommonColorConfig & { export type ContainerHeaderPaddingConfig = CommonColorConfig & { readonly containerHeaderPadding: string; - }; export type ContainerBodyPaddingConfig = CommonColorConfig & { @@ -407,13 +406,19 @@ const TEXT_TRANSFORM = { name: "textTransform", label: trans("style.textTransform"), textTransform: "textTransform" -} +} as const; const TEXT_DECORATION = { name: "textDecoration", label: trans("style.textDecoration"), textDecoration: "textDecoration" -} +} as const; + +const BORDER_STYLE = { + name: "borderStyle", + label: trans("style.borderStyle"), + borderStyle: "borderStyle" +} as const; const getStaticBorder = (color: string = SECOND_SURFACE_COLOR) => ({ @@ -440,6 +445,7 @@ const STYLING_FIELDS_SEQUENCE = [ FONT_FAMILY, FONT_STYLE, BORDER, + BORDER_STYLE, MARGIN, PADDING, RADIUS, @@ -696,7 +702,6 @@ export const ContainerFooterStyle = [ ] as const; export const SliderStyle = [ - LABEL, FILL, { name: "thumbBorder", @@ -716,14 +721,16 @@ export const SliderStyle = [ ] as const; export const InputLikeStyle = [ - LABEL, getStaticBackground(SURFACE_COLOR), ...STYLING_FIELDS_SEQUENCE, ...ACCENT_VALIDATE, ] as const; +export const LabelStyle = [ + ...replaceAndMergeMultipleStyles([...InputLikeStyle], 'text', [LABEL]).filter((style) => style.name !== 'radius' && style.name !== 'background') +] + export const RatingStyle = [ - LABEL, { name: "checked", label: trans("style.checked"), @@ -739,7 +746,6 @@ export const RatingStyle = [ ] as const; export const SwitchStyle = [ - LABEL, { name: "handle", label: trans("style.handle"), @@ -800,7 +806,7 @@ export const MultiSelectStyle = [ export const TabContainerStyle = [ // Keep background related properties of container as STYLING_FIELDS_SEQUENCE has rest of the properties - ...replaceAndMergeMultipleStyles([...ContainerStyle.filter((style)=> ['border','radius','borderWidth','margin','padding'].includes(style.name) === false),...STYLING_FIELDS_SEQUENCE], 'text', [{ + ...replaceAndMergeMultipleStyles([...ContainerStyle.filter((style) => ['border', 'radius', 'borderWidth', 'margin', 'padding'].includes(style.name) === false), ...STYLING_FIELDS_SEQUENCE], 'text', [{ name: "tabText", label: trans("style.tabText"), depName: "headerBackground", @@ -829,7 +835,6 @@ export const ModalStyle = [ ] as const; export const CascaderStyle = [ - LABEL, ...getStaticBgBorderRadiusByBg(SURFACE_COLOR, "pc"), TEXT, ACCENT, @@ -861,7 +866,7 @@ function checkAndUncheck() { } export const CheckboxStyle = [ - ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'text', [LABEL, STATIC_TEXT, VALIDATE]).filter((style) => style.name !== 'border'), + ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'text', [STATIC_TEXT, VALIDATE]).filter((style) => style.name !== 'border'), ...checkAndUncheck(), { name: "checked", @@ -874,7 +879,7 @@ export const CheckboxStyle = [ ] as const; export const RadioStyle = [ - ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'text', [LABEL, STATIC_TEXT, VALIDATE]).filter((style) => style.name !== 'border' && style.name !== 'radius'), + ...replaceAndMergeMultipleStyles(STYLING_FIELDS_SEQUENCE, 'text', [STATIC_TEXT, VALIDATE]).filter((style) => style.name !== 'border' && style.name !== 'radius'), ...checkAndUncheck(), { name: "checked", @@ -888,7 +893,7 @@ export const RadioStyle = [ export const SegmentStyle = [ LABEL, - ...STYLING_FIELDS_SEQUENCE.filter((style)=> ['border','borderWidth'].includes(style.name) === false), + ...STYLING_FIELDS_SEQUENCE.filter((style) => ['border', 'borderWidth'].includes(style.name) === false), { name: "indicatorBackground", label: trans("style.indicatorBackground"), @@ -1046,7 +1051,6 @@ export const FileViewerStyle = [ export const IframeStyle = [getBackground(), getStaticBorder("#00000000"), RADIUS, BORDER_WIDTH, MARGIN, PADDING] as const; export const DateTimeStyle = [ - LABEL, ...getStaticBgBorderRadiusByBg(SURFACE_COLOR), TEXT, MARGIN, @@ -1144,11 +1148,11 @@ export const ImageStyle = [getStaticBorder("#00000000"), RADIUS, BORDER_WIDTH, M export const IconStyle = [ getStaticBackground("#00000000"), - getStaticBorder("#00000000"), + getStaticBorder("#00000000"), FILL, RADIUS, BORDER_WIDTH, - MARGIN, + MARGIN, PADDING] as const; export const ListViewStyle = BG_STATIC_BORDER_RADIUS; @@ -1192,7 +1196,6 @@ export const TimeLineStyle = [ ] as const; export const TreeStyle = [ - LABEL, ...getStaticBgBorderRadiusByBg(SURFACE_COLOR), TEXT, VALIDATE, @@ -1249,7 +1252,6 @@ export const CalendarStyle = [ ] as const; export const SignatureStyle = [ - LABEL, ...getBgBorderRadiusByBg(), { name: "pen", @@ -1371,6 +1373,7 @@ export const RichTextEditorStyle = [ BORDER_WIDTH ] as const; +export type LabelStyleType = StyleConfigType; export type InputLikeStyleType = StyleConfigType; export type ButtonStyleType = StyleConfigType; export type ToggleButtonStyleType = StyleConfigType; diff --git a/client/packages/lowcoder/src/comps/index.tsx b/client/packages/lowcoder/src/comps/index.tsx index 17d768f42..4fefa935b 100644 --- a/client/packages/lowcoder/src/comps/index.tsx +++ b/client/packages/lowcoder/src/comps/index.tsx @@ -6,6 +6,7 @@ import { ButtonComp } from "./comps/buttonComp/buttonComp"; import { DropdownComp } from "./comps/buttonComp/dropdownComp"; import { LinkComp } from "./comps/buttonComp/linkComp"; import { ContainerComp, defaultContainerData } from "./comps/containerComp/containerComp"; +import { ContainerComp as FloatTextContainerComp } from "./comps/containerComp/textContainerComp"; import { CustomComp } from "./comps/customComp/customComp"; import { DatePickerComp, DateRangeComp } from "./comps/dateComp/dateComp"; import { DividerComp } from "./comps/dividerComp"; @@ -265,6 +266,25 @@ var uiCompMap: Registry = { }, defaultDataFn: defaultContainerData, }, + + floatTextContainer: { + name: trans("uiComp.floatTextContainerCompName"), + enName: "Container", + description: trans("uiComp.floatTextContainerCompDesc"), + categories: ["layout"], + icon: ContainerCompIcon, + keywords: trans("uiComp.floatTextContainerCompKeywords"), + comp: FloatTextContainerComp, + withoutLoading: true, + layoutInfo: { + w: 9, + h: 25, + // static: true, + delayCollision: true, + }, + defaultDataFn: defaultContainerData, + }, + tabbedContainer: { name: trans("uiComp.tabbedContainerCompName"), enName: "Tabbed Container", diff --git a/client/packages/lowcoder/src/comps/uiCompRegistry.ts b/client/packages/lowcoder/src/comps/uiCompRegistry.ts index c74893811..160b28503 100644 --- a/client/packages/lowcoder/src/comps/uiCompRegistry.ts +++ b/client/packages/lowcoder/src/comps/uiCompRegistry.ts @@ -97,6 +97,7 @@ export type UICompType = | "form" | "jsonSchemaForm" | "container" + | "floatTextContainer" | "tabbedContainer" | "modal" | "listView" diff --git a/client/packages/lowcoder/src/comps/utils/gridCompOperator.ts b/client/packages/lowcoder/src/comps/utils/gridCompOperator.ts index 828b8f286..6175e2794 100644 --- a/client/packages/lowcoder/src/comps/utils/gridCompOperator.ts +++ b/client/packages/lowcoder/src/comps/utils/gridCompOperator.ts @@ -77,6 +77,7 @@ export class GridCompOperator { return true; } + // FALK TODO: How can we enable Copy and Paste of components across Browser Tabs / Windows? static pasteComp(editorState: EditorState) { if (!this.copyComps || _.size(this.copyComps) <= 0 || !this.sourcePositionParams) { messageInstance.info(trans("gridCompOperator.selectCompFirst")); diff --git a/client/packages/lowcoder/src/constants/applicationConstants.ts b/client/packages/lowcoder/src/constants/applicationConstants.ts index 895d78153..0100e644d 100644 --- a/client/packages/lowcoder/src/constants/applicationConstants.ts +++ b/client/packages/lowcoder/src/constants/applicationConstants.ts @@ -130,7 +130,8 @@ export interface AppPermissionInfo { publicToMarketplace: boolean; } -export type AppViewMode = "edit" | "preview" | "view" | "view_marketplace"; +// adding viewMode for marketplace and adminMode for Admin area use +export type AppViewMode = "edit" | "preview" | "view" | "view_marketplace" | "admin"; export type AppPathParams = { viewMode: AppViewMode; diff --git a/client/packages/lowcoder/src/constants/routesURL.ts b/client/packages/lowcoder/src/constants/routesURL.ts index adc77006e..2570b34ac 100644 --- a/client/packages/lowcoder/src/constants/routesURL.ts +++ b/client/packages/lowcoder/src/constants/routesURL.ts @@ -20,6 +20,7 @@ export const PERMISSION_SETTING_DETAIL = `${PERMISSION_SETTING}/:groupId`; export const ORGANIZATION_SETTING_DETAIL = `${ORGANIZATION_SETTING}/:orgId`; export const ALL_APPLICATIONS_URL = "/apps"; +export const ADMIN_APP_URL = "/ee/:applicationId/:viewMode"; export const APPLICATION_MARKETPLACE_URL = `https://app.lowcoder.cloud/apps`; export const MODULE_APPLICATIONS_URL = "/apps/module"; export const MARKETPLACE_URL = `/marketplace`; diff --git a/client/packages/lowcoder/src/constants/styleSelectors.ts b/client/packages/lowcoder/src/constants/styleSelectors.ts index e022e94a2..fab74dbfd 100644 --- a/client/packages/lowcoder/src/constants/styleSelectors.ts +++ b/client/packages/lowcoder/src/constants/styleSelectors.ts @@ -7,4 +7,4 @@ export const CNSidebar = "sidebar"; export const CNSidebarItem = "sidebar-item"; export const CNSidebarSection = "sidebar-section"; export const CNMainContent = "main-content"; -export const CNRootContainer = "root-container"; +export const CNRootContainer = "lowcoder-app-canvas"; diff --git a/client/packages/lowcoder/src/i18n/locales/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index e782c9882..240173dd2 100644 --- a/client/packages/lowcoder/src/i18n/locales/de.ts +++ b/client/packages/lowcoder/src/i18n/locales/de.ts @@ -301,6 +301,7 @@ export const de = { "border": "Farbe der Umrandung", "borderRadius": "Radius der Grenze", "borderWidth": "Breite des Randes", + "borderStyle":"Grenzstil", "background": "Hintergrund", "headerBackground": "Kopfzeile Hintergrund", "footerBackground": "Fußzeilen-Hintergrund", diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index a42869c4e..d2c1530d0 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -322,6 +322,7 @@ export const en = { "border": "Border Color", "borderRadius": "Border Radius", "borderWidth": "Border Width", + "borderStyle":"Border Style", "background": "Background", "headerBackground": "Header Background", "footerBackground": "Footer Background", @@ -846,6 +847,10 @@ export const en = { "containerCompDesc": "A general-purpose container for layout and organization of UI elements.", "containerCompKeywords": "container, layout, organization, UI", + "floatTextContainerCompName": "Float Text Container", + "floatTextContainerCompDesc": "Float Text Container component", + "floatTextContainerCompKeywords": "container, layout, text, flow", + "collapsibleContainerCompName": "Collapsible Container", "collapsibleContainerCompDesc": "A container that can be expanded or collapsed, ideal for managing content visibility.", "collapsibleContainerCompKeywords": "collapsible, container, expand, collapse", @@ -1500,7 +1505,10 @@ export const en = { "mode": "Select Mode" }, "container": { - "title": "Displayed Container Title" + "title": "Displayed Container Title", + "titleTooltip": "The Title of the Container", + "flowWidth": "Content Width", + "floatType": "Text Float Type", }, "drawer": { "closePosition": "Close Button Placement", @@ -1584,7 +1592,8 @@ export const en = { "oauthProviders": "User Authentication", "appUsage": "App Usage Logs", "environments": "Environments", - "premium": "Premium" + "premium": "Premium", + "AppUsage": "Global App Usage", }, diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 3b18ce934..c2fb61742 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -311,6 +311,7 @@ style: { border: "边框颜色", borderRadius: "边框半径", borderWidth: "边框宽度", + borderStyle:"边框样式", background: "背景", headerBackground: "头部背景", footerBackground: "底部背景", @@ -799,6 +800,9 @@ uiComp: { containerCompName: "容器", containerCompDesc: "容器组件", containerCompKeywords: "", + floatTextContainerCompName: "浮动文本容器", + floatTextContainerCompDesc: "浮动文本容器组件", + floatTextContainerCompKeywords: "", collapsibleContainerCompName: "可折叠容器", collapsibleContainerCompDesc: "可折叠容器组件", collapsibleContainerCompKeywords: "", diff --git a/client/packages/lowcoder/src/layout/gridItem.tsx b/client/packages/lowcoder/src/layout/gridItem.tsx index ad1f8ae06..e0a3b41ed 100644 --- a/client/packages/lowcoder/src/layout/gridItem.tsx +++ b/client/packages/lowcoder/src/layout/gridItem.tsx @@ -131,8 +131,10 @@ export function GridItem(props: GridItemProps) { const mixinDraggable = (child: ReactElement, isDraggable: boolean): ReactElement => { const { i } = props as Required; + const testSelectorClass = `lowcoder-${props.compType}`; return (
{trans("settings.AppUsage")}, + routePath: "/ee/6600ae8724a23f365ba2ed4c/admin", + routePathExact: false, + routeComp: AppEditor, + icon: ({ selected, ...otherProps }) => selected ? ( ) : ( ), + visible: ({ user }) => user.orgDev, + }, + ], + } : { items: [] }, ]} /> {user.orgDev && ( diff --git a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx index c1d241a0a..cf8362c95 100644 --- a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx +++ b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx @@ -30,10 +30,12 @@ import { DatasourceApi } from "api/datasourceApi"; export default function AppEditor() { const showAppSnapshot = useSelector(showAppSnapshotSelector); - const isUserViewMode = useUserViewMode(); const params = useParams(); - const applicationId = params.applicationId; - const viewMode = params.viewMode === "view" ? "published" : params.viewMode === "view_marketplace" ? "view_marketplace" : "editing"; + const isUserViewModeCheck = useUserViewMode(); + const isUserViewMode = params.viewMode ? isUserViewModeCheck : true; + const applicationId = params.applicationId || window.location.pathname.split("/")[2]; + const paramViewMode = params.viewMode || window.location.pathname.split("/")[3]; + const viewMode = (paramViewMode === "view" || paramViewMode === "admin") ? "published" : paramViewMode === "view_marketplace" ? "view_marketplace" : "editing"; const currentUser = useSelector(getUser); const dispatch = useDispatch(); const fetchOrgGroupsFinished = useSelector(getFetchOrgGroupsFinished); @@ -42,7 +44,7 @@ export default function AppEditor() { const firstRendered = useRef(false); const [isDataSourcePluginRegistered, setIsDataSourcePluginRegistered] = useState(false); - setGlobalSettings({ applicationId, isViewMode: params.viewMode === "view" }); + setGlobalSettings({ applicationId, isViewMode: paramViewMode === "view" }); if (!firstRendered.current) { perfClear(); @@ -69,19 +71,19 @@ export default function AppEditor() { // fetch dataSource and plugin useEffect(() => { - if (!orgId || params.viewMode !== "edit") { + if (!orgId || paramViewMode !== "edit") { return; } dispatch(fetchDataSourceTypes({ organizationId: orgId })); dispatch(fetchFolderElements({})); - }, [dispatch, orgId, params.viewMode]); + }, [dispatch, orgId, paramViewMode]); useEffect(() => { - if (applicationId && params.viewMode === "edit") { + if (applicationId && paramViewMode === "edit") { dispatch(fetchDataSourceByApp({ applicationId: applicationId })); dispatch(fetchQueryLibraryDropdown()); } - }, [dispatch, applicationId, params.viewMode]); + }, [dispatch, applicationId, paramViewMode]); useEffect(() => { DatasourceApi.fetchJsDatasourceByApp(applicationId).then((res) => { diff --git a/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx b/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx index 0ebc77c8e..8320792dc 100644 --- a/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx +++ b/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx @@ -132,6 +132,7 @@ export function AppEditorInternalView(props: AppEditorInternalViewProps) { readOnly, appType: appInfo.appType, applicationId: appInfo.id, + hideHeader: window.location.pathname.split("/")[3] === "admin", ...extraExternalEditorState, })); }, [compInstance?.history, extraExternalEditorState, readOnly, appInfo.appType, appInfo.id]); @@ -146,8 +147,10 @@ export function AppEditorInternalView(props: AppEditorInternalViewProps) { !compInstance || !compInstance.comp || !compInstance.comp.preloaded || props.loading; return loading ? ( + window.location.pathname.split("/")[3] === "admin" ?
: ) : ( + // Falk - here we could add the language choise? {compInstance?.comp?.getView()} diff --git a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx index 5d69824e3..c0b04dcbe 100644 --- a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx @@ -43,7 +43,6 @@ import { AutoCompleteCompIcon, IconCompIcon, ResponsiveLayoutCompIcon, - } from "lowcoder-design"; export const CompStateIcon: { @@ -87,6 +86,7 @@ export const CompStateIcon: { form: , jsonSchemaForm: , container: , + floatTextContainer: , meeting: , mermaid: , videocomponent: , diff --git a/client/packages/lowcoder/src/pages/editor/editorView.tsx b/client/packages/lowcoder/src/pages/editor/editorView.tsx index af1981c37..9048c0959 100644 --- a/client/packages/lowcoder/src/pages/editor/editorView.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorView.tsx @@ -329,6 +329,9 @@ function EditorView(props: EditorViewProps) { const hideBodyHeader = useTemplateViewMode(); + // we check if we are on the public cloud + const isLowCoderDomain = window.location.hostname === 'app.lowcoder.cloud'; + if (readOnly && hideHeader) { return ( @@ -341,7 +344,19 @@ function EditorView(props: EditorViewProps) { if (readOnly && !showAppSnapshot) { return ( - {application && {application.name}} + + {application && {application.name}} + {isLowCoderDomain && [ + // Adding Support for iframely to be able to embedd the component explorer in the docu + , + , + , + , + , + // adding Clearbit Support for Analytics + + ]} + {!hideBodyHeader && } @@ -354,6 +369,7 @@ function EditorView(props: EditorViewProps) { ); } + // history mode, display with the right panel, a little trick const showRight = panelStatus.right || showAppSnapshot; let uiCompView; @@ -392,7 +408,19 @@ function EditorView(props: EditorViewProps) { toggleEditorModeStatus={toggleEditorModeStatus} editorModeStatus={editorModeStatus} /> - {application && {application.name}} + + {application && {application.name}} + {isLowCoderDomain && [ + // Adding Support for iframely to be able to embedd the component explorer in the docu + , + , + , + , + , + // adding Clearbit Support for Analytics + + ]} + {showNewUserGuide && }
{compMeta.name}
-
{compMeta.description || "No description."}
+
{compMeta.description || "No description."} v{packageVersion || ""}
); diff --git a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx index 427b586a8..67bc30861 100644 --- a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx +++ b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx @@ -7,7 +7,7 @@ import { getUser } from "redux/selectors/usersSelectors"; import { BluePlusIcon, CustomModal, DocLink, TacoButton, TacoInput } from "lowcoder-design"; import { getCommonSettings } from "redux/selectors/commonSettingSelectors"; import styled from "styled-components"; -import { normalizeNpmPackage, validateNpmPackage } from "comps/utils/remote"; +import { getNpmPackageMeta, normalizeNpmPackage, validateNpmPackage } from "comps/utils/remote"; import { ComListTitle, ExtensionContentWrapper } from "../styledComponent"; import { EmptyContent } from "components/EmptyContent"; import { messageInstance } from "lowcoder-design"; @@ -36,7 +36,7 @@ export default function PluginPanel() { }), [commonSettings?.npmPlugins] ); - + const handleSetNpmPlugins = (nextNpmPlugins: string[]) => { dispatch( setCommonSettings({ diff --git a/client/packages/lowcoder/src/util/dateTimeUtils.ts b/client/packages/lowcoder/src/util/dateTimeUtils.ts index 1cfdb99ae..4ec6cea67 100644 --- a/client/packages/lowcoder/src/util/dateTimeUtils.ts +++ b/client/packages/lowcoder/src/util/dateTimeUtils.ts @@ -1,6 +1,70 @@ +import { time } from "console"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; +import timezone from "dayjs/plugin/timezone"; +import duration from "dayjs/plugin/duration"; +import utc from "dayjs/plugin/utc"; +import quarterOfYear from "dayjs/plugin/quarterOfYear"; +import weekOfYear from "dayjs/plugin/weekOfYear"; +import isBetween from "dayjs/plugin/isBetween"; +import isToday from "dayjs/plugin/isToday"; +import isTomorrow from "dayjs/plugin/isTomorrow"; +import isYesterday from "dayjs/plugin/isYesterday"; +import customParseFormat from "dayjs/plugin/customParseFormat"; +import advancedFormat from "dayjs/plugin/advancedFormat"; +import updateLocale from "dayjs/plugin/updateLocale"; +import localeData from "dayjs/plugin/localeData"; +import localizedFormat from "dayjs/plugin/localizedFormat"; +import isLeapYear from "dayjs/plugin/isLeapYear"; +import isSameOrAfter from "dayjs/plugin/isSameOrAfter"; +import isSameOrBefore from "dayjs/plugin/isSameOrBefore"; +import isoWeek from "dayjs/plugin/isoWeek"; +import isoWeeksInYear from "dayjs/plugin/isoWeeksInYear"; +import weekYear from "dayjs/plugin/weekYear"; +import isMoment from "dayjs/plugin/isMoment"; +import calendar from "dayjs/plugin/calendar"; +import buddhistEra from "dayjs/plugin/buddhistEra"; +import minmax from "dayjs/plugin/minMax"; +import bigIntSupport from "dayjs/plugin/bigIntSupport"; +import objectSupport from "dayjs/plugin/objectSupport"; +import pluralGetSet from 'dayjs/plugin/pluralGetSet'; +import preParsePostFormat from 'dayjs/plugin/preParsePostFormat'; +import toObject from 'dayjs/plugin/toObject'; +import toArray from 'dayjs/plugin/toArray'; + +dayjs.extend(relativeTime); +dayjs.extend(timezone); +dayjs.extend(duration); +dayjs.extend(utc); +dayjs.extend(quarterOfYear); +dayjs.extend(weekOfYear); +dayjs.extend(isBetween); +dayjs.extend(isToday); +dayjs.extend(isTomorrow); +dayjs.extend(isYesterday); +dayjs.extend(customParseFormat); +dayjs.extend(advancedFormat); +dayjs.extend(updateLocale); +dayjs.extend(localeData); +dayjs.extend(localizedFormat); +dayjs.extend(isLeapYear); +dayjs.extend(isSameOrAfter); +dayjs.extend(isSameOrBefore); +dayjs.extend(isoWeek); +dayjs.extend(isoWeeksInYear); +dayjs.extend(weekYear); +dayjs.extend(isMoment); +dayjs.extend(calendar); +dayjs.extend(buddhistEra); +dayjs.extend(minmax); dayjs.extend(relativeTime); +dayjs.extend(bigIntSupport); +dayjs.extend(localizedFormat); +dayjs.extend(objectSupport); +dayjs.extend(pluralGetSet); +dayjs.extend(preParsePostFormat); +dayjs.extend(toObject); +dayjs.extend(toArray); export const TIME_FORMAT = "HH:mm:ss"; export const TIME_12_FORMAT = "HH:mm:ss:a"; diff --git a/client/packages/lowcoder/src/util/envUtils.ts b/client/packages/lowcoder/src/util/envUtils.ts index baa46eca0..852f1a7f5 100644 --- a/client/packages/lowcoder/src/util/envUtils.ts +++ b/client/packages/lowcoder/src/util/envUtils.ts @@ -13,6 +13,7 @@ export function developEnv(): boolean { /** * is enterprise edition */ +// Falk: TODO: check EE by API Call export function isEE(): boolean { return REACT_APP_EDITION === "enterprise" || REACT_APP_EDITION === "enterprise-global"; } diff --git a/client/packages/lowcoder/vite.config.mts b/client/packages/lowcoder/vite.config.mts index 816a9be20..2bba4a00a 100644 --- a/client/packages/lowcoder/vite.config.mts +++ b/client/packages/lowcoder/vite.config.mts @@ -17,9 +17,6 @@ dotenv.config(); const apiProxyTarget = process.env.LOWCODER_API_SERVICE_URL; const nodeServiceApiProxyTarget = process.env.NODE_SERVICE_API_PROXY_TARGET; const nodeEnv = process.env.NODE_ENV ?? "development"; -const edition = process.env.REACT_APP_EDITION; -const isEEGlobal = edition === "enterprise-global"; -const isEE = edition === "enterprise" || isEEGlobal; const isDev = nodeEnv === "development"; const isVisualizerEnabled = !!process.env.ENABLE_VISUALIZER; // the file was never created @@ -61,8 +58,7 @@ export const viteConfig: UserConfig = { extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json"], alias: { "@lowcoder-ee": path.resolve( - __dirname, - isEE ? `../lowcoder/src/${isEEGlobal ? "ee-global" : "ee"}` : "../lowcoder/src" + __dirname, "../lowcoder/src" ), }, }, diff --git a/client/packages/lowcoder/vite.config.mts.timestamp-1702455580530-4609d841cb7.mjs b/client/packages/lowcoder/vite.config.mts.timestamp-1702455580530-4609d841cb7.mjs index 502280ea8..98ae2b873 100644 --- a/client/packages/lowcoder/vite.config.mts.timestamp-1702455580530-4609d841cb7.mjs +++ b/client/packages/lowcoder/vite.config.mts.timestamp-1702455580530-4609d841cb7.mjs @@ -173,9 +173,6 @@ dotenv.config(); var apiProxyTarget = process.env.LOWCODER_API_SERVICE_URL; var nodeServiceApiProxyTarget = process.env.NODE_SERVICE_API_PROXY_TARGET; var nodeEnv = process.env.NODE_ENV ?? "development"; -var edition = process.env.REACT_APP_EDITION; -var isEEGlobal = edition === "enterprise-global"; -var isEE = edition === "enterprise" || isEEGlobal; var isDev = nodeEnv === "development"; var isVisualizerEnabled = !!process.env.ENABLE_VISUALIZER; var browserCheckFileName = `browser-check.js`; @@ -209,8 +206,7 @@ var viteConfig = { extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json"], alias: { "@lowcoder-ee": path.resolve( - __vite_injected_original_dirname, - isEE ? `../lowcoder/src/${isEEGlobal ? "ee-global" : "ee"}` : "../lowcoder/src" + __vite_injected_original_dirname, "../lowcoder/src" ) } }, diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 6f55ed0fc..c536b8a24 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -2,20 +2,14 @@ ## Build Lowcoder api-service application ## FROM maven:3.9-eclipse-temurin-17 AS build-api-service + +# Build lowcoder-api COPY ./server/api-service /lowcoder-server WORKDIR /lowcoder-server RUN --mount=type=cache,target=/root/.m2 mvn -f pom.xml clean package -DskipTests # Create required folder structure -RUN mkdir -p /lowcoder/api-service/plugins /lowcoder/api-service/config /lowcoder/api-service/logs - -# Define lowcoder main jar and plugin jars -ARG JAR_FILE=/lowcoder-server/lowcoder-server/target/lowcoder-server-*.jar -ARG PLUGIN_JARS=/lowcoder-server/lowcoder-plugins/*/target/*.jar - -# Copy lowcoder server application and plugins -RUN cp ${JAR_FILE} /lowcoder/api-service/server.jar \ - && cp ${PLUGIN_JARS} /lowcoder/api-service/plugins/ +RUN mkdir -p /lowcoder/api-service/config /lowcoder/api-service/logs /lowcoder/plugins # Copy lowcoder server configuration COPY server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml /lowcoder/api-service/config/ @@ -43,6 +37,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends gosu \ # Copy lowcoder server configuration COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder/api-service /lowcoder/api-service +# Copy lowcoder api service app, dependencies and libs +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/app /lowcoder/api-service/app +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/dependencies /lowcoder/api-service/dependencies +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/libs /lowcoder/api-service/libs +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/plugins /lowcoder/api-service/plugins +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/set-classpath.sh /lowcoder/api-service/set-classpath.sh + EXPOSE 8080 CMD [ "sh" , "/lowcoder/api-service/entrypoint.sh" ] @@ -202,6 +203,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-instal # Add lowcoder api-service COPY --chown=lowcoder:lowcoder --from=lowcoder-ce-api-service /lowcoder/api-service /lowcoder/api-service +RUN mkdir -p /lowcoder/plugins/ && chown lowcoder:lowcoder /lowcoder/plugins/ # Add lowcoder node-service COPY --chown=lowcoder:lowcoder --from=lowcoder-ce-node-service /lowcoder/node-service /lowcoder/node-service diff --git a/deploy/docker/api-service/entrypoint.sh b/deploy/docker/api-service/entrypoint.sh index 5f2e3ad2e..0f43580fe 100644 --- a/deploy/docker/api-service/entrypoint.sh +++ b/deploy/docker/api-service/entrypoint.sh @@ -27,12 +27,16 @@ ${JAVA_HOME}/bin/java -version echo cd /lowcoder/api-service +source set-classpath.sh + exec gosu ${USER_ID}:${GROUP_ID} ${JAVA_HOME}/bin/java \ + -Djava.util.prefs.userRoot=/tmp \ -Djava.security.egd=file:/dev/./urandom \ -Dhttps.protocols=TLSv1.1,TLSv1.2 \ -Dlog4j2.formatMsgNoLookups=true \ -Dspring.config.location="file:///lowcoder/api-service/config/application.yml,file:///lowcoder/api-service/config/application-selfhost.yml" \ --add-opens java.base/java.nio=ALL-UNNAMED \ + -cp "${LOWCODER_CLASSPATH:=.}" \ ${JAVA_OPTS} \ - -jar "${APP_JAR}" --spring.webflux.base-path=${CONTEXT_PATH} ${CUSTOM_APP_PROPERTIES} + org.lowcoder.api.ServerApplication --spring.webflux.base-path=${CONTEXT_PATH} ${CUSTOM_APP_PROPERTIES} diff --git a/server/api-service/.gitignore b/server/api-service/.gitignore index 044c6298e..a9fc541a9 100644 --- a/server/api-service/.gitignore +++ b/server/api-service/.gitignore @@ -23,8 +23,9 @@ dependency-reduced-pom.xml .run/** logs/** tmp/** -/openblocks-server/logs/ +# Ignore plugin.properties which are generated dynamically +**/plugin.properties # to ignore the node_modeules folder node_modules @@ -34,5 +35,4 @@ package-lock.json # test coverage coverage-summary.json app/client/cypress/locators/Widgets.json -/openblocks-domain/logs/ -application-lowcoder.yml \ No newline at end of file +application-lowcoder.yml diff --git a/server/api-service/PLUGIN.md b/server/api-service/PLUGIN.md new file mode 100644 index 000000000..65a99adef --- /dev/null +++ b/server/api-service/PLUGIN.md @@ -0,0 +1,63 @@ +# Lowcoder backend plugin system + +This is an ongoing effort to refactor current plugin system based on pf4j library. + +## Reasoning + +1. create a cleaner and simpler plugin system with clearly defined purpose(s) (new endpoints, new datasource types, etc..) +2. lowcoder does not need live plugin loading/reloading/unloading/updates, therefore the main feature of pf4j is rendered useless, in fact it adds a lot of complexity due to classloaders used for managing plugins (especially in spring/boot applications) +3. simpler and easier plugin detection - just a jar with a class implementing a common interface (be it a simple pojo project or a complex spring/boot implementation) + +## How it works + +The main entrypoint for plugin system is in **lowcoder-server** module with class **org.lowcoder.api.framework.configuration.PluginConfiguration** +It creates: +- LowcoderPluginManager bean which is responsible for plugin lifecycle management +- Adds plugin defined endpoints to lowcoder by creating **pluginEndpoints** bean +- TODO: Adds plugin defined datasources to lowcoder by creating **pluginDatasources** bean + +### lowcoder-plugin-api library + +This library contains APIs for plugin implementations. +It is used by both, lowcoder API server as well as all plugins. + +### PluginLoader + +The sole purpose of a PluginLoader is to find plugin candidates and load them into VM. +There is currently one implementation that based on paths - **PathBasedPluginLoader**, it: +- looks in folders and subfolders defined in **application.yaml** - entries can point to a folder or specific jar file. If a relative path is supplied, the location of lowcoder API server application jar is used as parent folder (when run in non-packaged state, eg. in IDE, it uses the folder where ServerApplication.class is generated) + +```yaml +common: + plugin-dirs: + - plugins + - /some/custom/path/myGreatPlugin.jar +``` +- finds all **jar**(s) and inspects them for classes implementing **LowcoderPlugin** interface +- instantiates all LowcoderPlugin implementations + +### LowcoderPluginManager + +The main job of plugin manager is to: +- register plugins found and instantiated by **PluginLoader** +- start registered plugins by calling **LowcoderPlugin.load()** method +- create and register **RouterFunction**(s) for all loaded plugin endpoints +- TODO: create and register datasources for all loaded plugin datasources + +## Plugin project structure + +Plugin jar can be structured in any way you like. It can be a plain java project, but also a spring/boot based project or based on any other framework. + +It is composed from several parts: +- class(es) implementing **LowcoderPlugin** interface +- class(es) implementing **PluginEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format: + +```java + @EndpointExtension(uri = , method = ) + public EndpointResponse (EndpointRequest request) + { + ... your endpoint logic implementation + } +``` +- TODO: class(es) impelemting **LowcoderDatasource** interface + diff --git a/server/api-service/distribution/pom.xml b/server/api-service/distribution/pom.xml new file mode 100644 index 000000000..d68b3fab4 --- /dev/null +++ b/server/api-service/distribution/pom.xml @@ -0,0 +1,84 @@ + + 4.0.0 + + org.lowcoder + lowcoder-root + ${revision} + + + distribution + pom + + + ${project.build.directory}/dependencies + + + + + + + org.lowcoder + lowcoder-sdk + + + org.lowcoder + lowcoder-infra + + + org.lowcoder + lowcoder-domain + + + org.lowcoder + lowcoder-server + + + + + lowcoder-api-service + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${assembly.lib.directory} + false + false + true + true + + + + + + maven-assembly-plugin + + + distro-assembly + package + + single + + + false + + src/assembly/bin.xml + + + + + + + + + \ No newline at end of file diff --git a/server/api-service/distribution/src/assembly/bin.xml b/server/api-service/distribution/src/assembly/bin.xml new file mode 100644 index 000000000..b6422619e --- /dev/null +++ b/server/api-service/distribution/src/assembly/bin.xml @@ -0,0 +1,72 @@ + + bin + + dir + + false + + + + src/assembly/set-classpath.sh + + + + + + ${assembly.lib.directory} + dependencies + + ${project.groupId}:* + + + + + + + + true + + org.lowcoder:lowcoder-server + + + app + false + false + + + + + + true + + org.lowcoder:lowcoder-domain + org.lowcoder:lowcoder-infra + org.lowcoder:lowcoder-sdk + + + libs + false + false + + + + + + true + true + + org.lowcoder:*Plugin + + + org.lowcoder:sqlBasedPlugin + + + plugins + false + false + + + + \ No newline at end of file diff --git a/server/api-service/distribution/src/assembly/set-classpath.sh b/server/api-service/distribution/src/assembly/set-classpath.sh new file mode 100755 index 000000000..de82ddf7f --- /dev/null +++ b/server/api-service/distribution/src/assembly/set-classpath.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# +# Set lowcoder api service classpath for use in startup script +# +export LOWCODER_CLASSPATH="`find libs/ dependencies/ app/ -type f -name "*.jar" | tr '\n' ':' | sed -e 's/:$//'`" + +# +# Example usage: +# +# java -cp "${LOWCODER_CLASSPATH}" org.lowcoder.api.ServerApplication diff --git a/server/api-service/lowcoder-dependencies/pom.xml b/server/api-service/lowcoder-dependencies/pom.xml new file mode 100644 index 000000000..23ad18c48 --- /dev/null +++ b/server/api-service/lowcoder-dependencies/pom.xml @@ -0,0 +1,228 @@ + + + + + lowcoder-root + org.lowcoder + ${revision} + + + 4.0.0 + lowcoder-dependencies + pom + + + + + org.springframework.boot + spring-boot-dependencies + 3.1.2 + pom + import + + + + org.lowcoder.plugin + lowcoder-plugin-api + 2.3.0 + + + + org.pf4j + pf4j + 3.5.0 + + + + org.json + json + 20230227 + + + + org.projectlombok + lombok + 1.18.26 + + + + org.apache.commons + commons-text + 1.10.0 + + + commons-io + commons-io + 2.13.0 + + + org.glassfish + javax.el + 3.0.0 + + + javax.el + javax.el-api + 3.0.0 + jakarta.el + jakarta.el-api + 5.0.1 + + + org.eclipse.jgit + org.eclipse.jgit + 6.5.0.202303070854-r + + + + org.apache.commons + commons-collections4 + 4.4 + + + com.google.guava + guava + 30.0-jre + + + + tv.twelvetone.rjson + rjson + 1.3.1-SNAPSHOT + + + org.jetbrains.kotlin + kotlin-stdlib-jdk7 + 1.6.21 + + + + com.jayway.jsonpath + json-path + 2.7.0 + + + com.github.ben-manes.caffeine + caffeine + 3.0.5 + + + es.moki.ratelimitj + ratelimitj-core + 0.7.0 + + + com.github.spullara.mustache.java + compiler + 0.9.6 + + + + es.moki.ratelimitj + ratelimitj-redis + 0.7.0 + + + + io.projectreactor + reactor-core + 3.4.29 + + + + org.pf4j + pf4j-spring + 0.8.0 + + + + com.querydsl + querydsl-apt + 5.0.0 + + + + io.sentry + sentry-spring-boot-starter + 3.1.2 + + + + org.jgrapht + jgrapht-core + 1.5.0 + + + + jakarta.xml.bind + jakarta.xml.bind-api + 4.0.2 + + + jakarta.activation + jakarta.activation-api + 2.1.3 + + + + + + org.glassfish.jaxb + jaxb-runtime + 2.3.3 + + + + com.github.cloudyrock.mongock + mongock-bom + 4.3.8 + pom + import + + + + io.projectreactor.tools + blockhound + 1.0.6.RELEASE + + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + + + + io.projectreactor + reactor-test + 3.3.5.RELEASE + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo.spring30x + 4.7.0 + + + org.mockito + mockito-inline + 5.2.0 + test + + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + + + + + diff --git a/server/api-service/lowcoder-domain/pom.xml b/server/api-service/lowcoder-domain/pom.xml index 2150c484a..9c25011d0 100644 --- a/server/api-service/lowcoder-domain/pom.xml +++ b/server/api-service/lowcoder-domain/pom.xml @@ -5,7 +5,7 @@ lowcoder-root org.lowcoder - ${revision} + ${revision} 4.0.0 @@ -50,6 +50,10 @@ org.lowcoder lowcoder-infra + + org.springframework.boot + spring-boot-starter-mail + com.github.cloudyrock.mongock @@ -186,6 +190,12 @@ es.moki.ratelimitj ratelimitj-redis + + + io.lettuce + lettuce-core + + @@ -224,13 +234,13 @@ test - - javax.xml.bind - jaxb-api + + jakarta.xml.bind + jakarta.xml.bind-api - - javax.activation - activation + + jakarta.activation + jakarta.activation-api @@ -242,6 +252,18 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + -parameters + + + + com.mysema.maven apt-maven-plugin @@ -268,9 +290,21 @@ UTF-8 + UTF-8 + 17 - 17 - 17 + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + + diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/ApplicationUtil.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/ApplicationUtil.java index c74b186c5..b143c1061 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/ApplicationUtil.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/ApplicationUtil.java @@ -5,12 +5,13 @@ import java.util.Map; import java.util.Set; -import javax.validation.constraints.NotNull; import org.apache.commons.lang3.StringUtils; import com.google.common.collect.Sets; +import jakarta.validation.constraints.NotNull; + public class ApplicationUtil { public static Object getContainerSizeFromDSL(Map dsl) { diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/asset/service/AssetServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/asset/service/AssetServiceImpl.java index 405734f47..ff802c5d7 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/asset/service/AssetServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/asset/service/AssetServiceImpl.java @@ -61,7 +61,7 @@ public Mono upload(Part filePart, int maxFileSizeKB, boolean isThumbnail) // The reason we restrict file types here is to avoid having to deal with dangerous image types such as SVG, // which can have arbitrary HTML/JS inside of them. - + final MediaType contentType = filePart.headers().getContentType(); if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) { return Mono.error(new BizException(BizError.INVALID_PARAMETER, "INCORRECT_IMAGE_TYPE")); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java deleted file mode 100644 index 18d73fdf5..000000000 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.lowcoder.domain.configurations; - -import org.pf4j.spring.SpringPluginManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class Pf4jConfiguration { - - @Bean - public SpringPluginManager pluginManager() { - return new SpringPluginManager(); - } - -} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java index a254da0f1..88bc8b7da 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java @@ -17,4 +17,9 @@ public class Folder extends HasIdAndAuditing { @Nullable private String parentFolderId; // null represents folder in the root folder private String name; + private String title; + private String description; + private String category; + private String type; + private String image; } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/Group.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/Group.java index 5e44801b1..cc2c779b2 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/Group.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/Group.java @@ -5,7 +5,6 @@ import java.util.Locale; import javax.annotation.Nonnull; -import javax.validation.constraints.NotNull; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; @@ -15,6 +14,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; import lombok.ToString; diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java index 2a754bb6d..634a8cdb1 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/GroupMember.java @@ -36,6 +36,11 @@ public boolean isAdmin() { return role == MemberRole.ADMIN; } + public boolean isSuperAdmin() { + return role == MemberRole.SUPER_ADMIN; + } + + @JsonIgnore public boolean isInvalid() { return this == NOT_EXIST || StringUtils.isBlank(groupId); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/repository/GroupRepository.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/repository/GroupRepository.java index bcf702532..219c08db5 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/repository/GroupRepository.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/repository/GroupRepository.java @@ -2,12 +2,11 @@ import java.util.Collection; -import javax.validation.constraints.NotNull; - import org.lowcoder.domain.group.model.Group; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.stereotype.Repository; +import jakarta.validation.constraints.NotNull; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java index 5aefdbae6..7e7a9daf0 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/MemberRole.java @@ -7,7 +7,8 @@ public enum MemberRole { MEMBER("member"), - ADMIN("admin"); + ADMIN("admin"), + SUPER_ADMIN("super_admin"); private static final Map VALUE_MAP; diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java index 66e83f49e..5e990485a 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/OrgMember.java @@ -52,6 +52,10 @@ public MemberRole getRole() { return role; } + public boolean isSuperAdmin() { + return role == MemberRole.SUPER_ADMIN; + } + public boolean isAdmin() { return role == MemberRole.ADMIN; } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java index 984788f5a..21584ba8f 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/model/Organization.java @@ -86,7 +86,7 @@ public OrganizationCommonSettings getCommonSettings() { public static class OrganizationCommonSettings extends HashMap { public static final String USER_EXTRA_TRANSFORMER = "userExtraTransformer"; public static final String USER_EXTRA_TRANSFORMER_UPDATE_TIME = "userExtraTransformer_updateTime"; - + public static final String PASSWORD_RESET_EMAIL_TEMPLATE = "passwordResetEmailTemplate"; // custom branding configs public static final String CUSTOM_BRANDING_KEY = "branding"; } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java index 4dc918374..5a4d82ec6 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationService.java @@ -17,9 +17,9 @@ public interface OrganizationService { @PossibleEmptyMono Mono getOrganizationInEnterpriseMode(); - Mono create(Organization organization, String creatorUserId); + Mono create(Organization organization, String creatorUserId, boolean isSuperAdmin); - Mono createDefault(User user); + Mono createDefault(User user, boolean isSuperAdmin); Mono getById(String id); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java index 48e4bc6de..55abc2a76 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java @@ -1,6 +1,7 @@ package org.lowcoder.domain.organization.service; import static org.lowcoder.domain.authentication.AuthenticationService.DEFAULT_AUTH_CONFIG; +import static org.lowcoder.domain.organization.model.Organization.OrganizationCommonSettings.PASSWORD_RESET_EMAIL_TEMPLATE; import static org.lowcoder.domain.organization.model.OrganizationState.ACTIVE; import static org.lowcoder.domain.organization.model.OrganizationState.DELETED; import static org.lowcoder.domain.util.QueryDslUtils.fieldName; @@ -56,6 +57,12 @@ public class OrganizationServiceImpl implements OrganizationService { private final Conf logoMaxSizeInKb; + private static final String PASSWORD_RESET_EMAIL_TEMPLATE_DEFAULT = "

Hi, %s
" + + "Here is the link to reset your password: %s
" + + "Please note that the link will expire after 12 hours.

" + + "Regards,
" + + "The Lowcoder Team

"; + @Autowired private AssetRepository assetRepository; @@ -86,7 +93,7 @@ public OrganizationServiceImpl(ConfigCenter configCenter) { } @Override - public Mono createDefault(User user) { + public Mono createDefault(User user, boolean isSuperAdmin) { return Mono.deferContextual(contextView -> { Locale locale = getLocale(contextView); String userOrgSuffix = getMessage(locale, "USER_ORG_SUFFIX"); @@ -96,7 +103,7 @@ public Mono createDefault(User user) { organization.setIsAutoGeneratedOrganization(true); // saas mode if (commonConfig.getWorkspace().getMode() == WorkspaceMode.SAAS) { - return create(organization, user.getId()); + return create(organization, user.getId(), isSuperAdmin); } // enterprise mode return joinOrganizationInEnterpriseMode(user.getId()) @@ -107,7 +114,7 @@ public Mono createDefault(User user) { OrganizationDomain organizationDomain = new OrganizationDomain(); organizationDomain.setConfigs(List.of(DEFAULT_AUTH_CONFIG)); organization.setOrganizationDomain(organizationDomain); - return create(organization, user.getId()); + return create(organization, user.getId(), isSuperAdmin); }); }); } @@ -145,29 +152,32 @@ private Mono getByEnterpriseOrgId() { } @Override - public Mono create(Organization organization, String creatorId) { + public Mono create(Organization organization, String creatorId, boolean isSuperAdmin) { return Mono.defer(() -> { if (organization == null || StringUtils.isNotBlank(organization.getId())) { return Mono.error(new BizException(BizError.INVALID_PARAMETER, "INVALID_PARAMETER", FieldName.ORGANIZATION)); } + organization.setCommonSettings(new OrganizationCommonSettings()); + organization.getCommonSettings().put("PASSWORD_RESET_EMAIL_TEMPLATE", + PASSWORD_RESET_EMAIL_TEMPLATE_DEFAULT); organization.setState(ACTIVE); return Mono.just(organization); }) .flatMap(repository::save) - .flatMap(newOrg -> onOrgCreated(creatorId, newOrg)) + .flatMap(newOrg -> onOrgCreated(creatorId, newOrg, isSuperAdmin)) .log(); } - private Mono onOrgCreated(String userId, Organization newOrg) { + private Mono onOrgCreated(String userId, Organization newOrg, boolean isSuperAdmin) { return groupService.createAllUserGroup(newOrg.getId()) .then(groupService.createDevGroup(newOrg.getId())) - .then(setOrgAdmin(userId, newOrg)) + .then(setOrgAdmin(userId, newOrg, isSuperAdmin)) .thenReturn(newOrg); } - private Mono setOrgAdmin(String userId, Organization newOrg) { - return orgMemberService.addMember(newOrg.getId(), userId, MemberRole.ADMIN); + private Mono setOrgAdmin(String userId, Organization newOrg, boolean isSuperAdmin) { + return orgMemberService.addMember(newOrg.getId(), userId, isSuperAdmin ? MemberRole.SUPER_ADMIN : MemberRole.ADMIN); } @Override diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java index 8b0587480..3841a42b9 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionHandler.java @@ -66,7 +66,7 @@ public Mono>> getAllMatchingPermissions(Str return getOrgId(resourceIds.iterator().next()) .flatMap(orgId -> orgMemberService.getOrgMember(orgId, userId)) .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.just(buildAdminPermissions(resourceType, resourceIds, userId)); } return getAllMatchingPermissions0(userId, orgMember.getOrgId(), resourceType, resourceIds, resourceAction); @@ -112,7 +112,7 @@ public Mono checkUserPermissionStatusOnResource( Mono orgUserPermissionMono = getOrgId(resourceId) .flatMap(orgId -> orgMemberService.getOrgMember(orgId, userId)) .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.just(UserPermissionOnResourceStatus.success(buildAdminPermission(resourceType, resourceId, userId))); } return getAllMatchingPermissions0(userId, orgMember.getOrgId(), resourceType, Collections.singleton(resourceId), resourceAction) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionService.java index 8dd06e9d4..13900333b 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/permission/service/ResourcePermissionService.java @@ -15,8 +15,8 @@ import java.util.Map.Entry; import java.util.Set; -import javax.annotation.Nullable; -import javax.validation.constraints.NotNull; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; import org.apache.commons.collections4.CollectionUtils; import org.lowcoder.domain.application.model.ApplicationRequestType; diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/Connection.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/Connection.java index 3dbd91e27..050b82081 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/Connection.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/Connection.java @@ -6,8 +6,8 @@ import java.util.Map; import java.util.Set; -import javax.annotation.Nullable; -import javax.validation.constraints.NotEmpty; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotEmpty; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.SetUtils; diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java index 80efd4ac5..a8350e0b5 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java @@ -3,6 +3,7 @@ import static com.google.common.base.Suppliers.memoize; import static org.lowcoder.infra.util.AssetUtils.toAssetPath; +import java.time.Instant; import java.util.*; import java.util.function.Supplier; @@ -52,6 +53,10 @@ public class User extends HasIdAndAuditing implements BeforeMongodbWrite, AfterM @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; + private String passwordResetToken; + + private Instant passwordResetTokenExpiry; + @Transient Boolean isAnonymous = false; diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java new file mode 100644 index 000000000..439aa65a8 --- /dev/null +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/EmailCommunicationService.java @@ -0,0 +1,49 @@ +package org.lowcoder.domain.user.service; + +import jakarta.mail.internet.MimeMessage; +import lombok.extern.slf4j.Slf4j; +import org.lowcoder.sdk.config.CommonConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +@Service +@Slf4j(topic = "EmailCommunicationService") +public class EmailCommunicationService { + + @Autowired + private JavaMailSender javaMailSender; + + @Autowired + private CommonConfig config; + + public boolean sendPasswordResetEmail(String to, String token, String message) { + try { + String subject = "Reset Your Lost Password"; + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true); + + mimeMessageHelper.setFrom(config.getLostPasswordEmailSender()); + mimeMessageHelper.setTo(to); + mimeMessageHelper.setSubject(subject); + + // Construct the message with the token link + String resetLink = config.getLowcoderPublicUrl() + "/lost-password?token=" + token; + String formattedMessage = String.format(message, to, resetLink); + mimeMessageHelper.setText(formattedMessage, true); // Set HTML to true to allow links + + javaMailSender.send(mimeMessage); + + return true; + + } catch (Exception e) { + log.error("Failed to send mail to: {}, Exception: ", to, e); + return false; + } + + + } + +} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java index aebed82ef..667f48c44 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java @@ -50,6 +50,10 @@ public interface UserService { Mono resetPassword(String userId); + Mono lostPassword(String userEmail); + + Mono resetLostPassword(String userEmail, String token, String newPassword); + Mono setPassword(String userId, String password); Mono buildUserDetail(User user, boolean withoutDynamicGroups); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index e7526be8d..056bd69dc 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -16,6 +16,7 @@ import org.lowcoder.domain.group.service.GroupService; import org.lowcoder.domain.organization.model.OrgMember; import org.lowcoder.domain.organization.service.OrgMemberService; +import org.lowcoder.domain.organization.service.OrganizationService; import org.lowcoder.domain.user.model.*; import org.lowcoder.domain.user.model.User.TransformedUserInfo; import org.lowcoder.domain.user.repository.UserRepository; @@ -29,6 +30,7 @@ import org.lowcoder.sdk.constants.WorkspaceMode; import org.lowcoder.sdk.exception.BizError; import org.lowcoder.sdk.exception.BizException; +import org.lowcoder.sdk.util.HashUtils; import org.lowcoder.sdk.util.LocaleUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; @@ -40,6 +42,8 @@ import javax.annotation.Nonnull; import java.security.SecureRandom; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -69,12 +73,15 @@ public class UserServiceImpl implements UserService { @Autowired private OrgMemberService orgMemberService; @Autowired + private OrganizationService organizationService; + @Autowired private GroupService groupService; @Autowired private CommonConfig commonConfig; @Autowired private AuthenticationService authenticationService; - + @Autowired + private EmailCommunicationService emailCommunicationService; private Conf avatarMaxSizeInKb; @PostConstruct @@ -262,6 +269,47 @@ public Mono resetPassword(String userId) { }); } + @Override + public Mono lostPassword(String userEmail) { + return findByName(userEmail) + .zipWhen(user -> orgMemberService.getCurrentOrgMember(user.getId()) + .flatMap(orgMember -> organizationService.getById(orgMember.getOrgId())) + .map(organization -> organization.getCommonSettings().get("PASSWORD_RESET_EMAIL_TEMPLATE"))) + .flatMap(tuple -> { + User user = tuple.getT1(); + String emailTemplate = (String)tuple.getT2(); + + String token = generateNewRandomPwd(); + Instant tokenExpiry = Instant.now().plus(12, ChronoUnit.HOURS); + if (!emailCommunicationService.sendPasswordResetEmail(userEmail, token, emailTemplate)) { + return Mono.empty(); + } + user.setPasswordResetToken(HashUtils.hash(token.getBytes())); + user.setPasswordResetTokenExpiry(tokenExpiry); + return repository.save(user).then(Mono.empty()); + }); + } + + @Override + public Mono resetLostPassword(String userEmail, String token, String newPassword) { + return findByName(userEmail) + .flatMap(user -> { + if (Instant.now().until(user.getPasswordResetTokenExpiry(), ChronoUnit.MINUTES) <= 0) { + return ofError(BizError.INVALID_PARAMETER, "TOKEN_EXPIRED"); + } + + if (!StringUtils.equals(HashUtils.hash(token.getBytes()), user.getPasswordResetToken())) { + return ofError(BizError.INVALID_PARAMETER, "INVALID_TOKEN"); + } + + user.setPassword(encryptionService.encryptPassword(newPassword)); + user.setPasswordResetToken(StringUtils.EMPTY); + user.setPasswordResetTokenExpiry(Instant.now()); + return repository.save(user) + .thenReturn(true); + }); + } + @SuppressWarnings("SpellCheckingInspection") @Nonnull private static String generateNewRandomPwd() { @@ -335,7 +383,7 @@ protected Mono>> buildUserDetailGroups(String userId, O Locale locale) { String orgId = orgMember.getOrgId(); Flux groups; - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { groups = groupService.getByOrgId(orgId).sort(); } else { if (withoutDynamicGroups) { diff --git a/server/api-service/lowcoder-infra/pom.xml b/server/api-service/lowcoder-infra/pom.xml index 5c34fde9c..39a8a8640 100644 --- a/server/api-service/lowcoder-infra/pom.xml +++ b/server/api-service/lowcoder-infra/pom.xml @@ -127,14 +127,33 @@ org.springframework.boot spring-boot-starter-webflux
+ + org.lowcoder.plugin + lowcoder-plugin-api + UTF-8 + UTF-8 + 17 + 17 17 + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + + diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/conditional/ConditionalOnPropertyNotBlank.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/conditional/ConditionalOnPropertyNotBlank.java index e4aca0672..0e698a68b 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/conditional/ConditionalOnPropertyNotBlank.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/conditional/ConditionalOnPropertyNotBlank.java @@ -6,8 +6,8 @@ import java.lang.annotation.Target; import java.util.Map; -import javax.annotation.Nullable; -import javax.validation.constraints.NotBlank; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotBlank; import org.apache.commons.lang3.StringUtils; import org.lowcoder.infra.conditional.ConditionalOnPropertyNotBlank.OnPropertyNotBlankCondition; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/APICallEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/APICallEvent.java new file mode 100644 index 000000000..f000e640f --- /dev/null +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/APICallEvent.java @@ -0,0 +1,21 @@ +package org.lowcoder.infra.event; + +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.springframework.util.MultiValueMap; + +@Getter +@SuperBuilder +public class APICallEvent extends AbstractEvent { + + private final EventType type; + private final String httpMethod; + private final String requestUri; + private final MultiValueMap headers; + private final MultiValueMap queryParams; + + @Override + public EventType getEventType() { + return EventType.API_CALL_EVENT; + } +} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java index 018ec9894..c11381cd2 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/AbstractEvent.java @@ -1,12 +1,56 @@ package org.lowcoder.infra.event; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import org.lowcoder.plugin.api.event.LowcoderEvent; + import lombok.Getter; import lombok.experimental.SuperBuilder; @Getter @SuperBuilder -public abstract class AbstractEvent implements Event { - +public abstract class AbstractEvent implements LowcoderEvent +{ protected final String orgId; protected final String userId; + protected final String sessionHash; + protected final Boolean isAnonymous; + private final String ipAddress; + protected Map details; + + public Map details() + { + return this.details; + } + + public static abstract class AbstractEventBuilder> + { + public B detail(String name, String value) + { + if (details == null) + { + details = new HashMap<>(); + } + this.details.put(name, value); + return self(); + } + } + + public void populateDetails() { + if (details == null) { + details = new HashMap<>(); + } + for(Field f : getClass().getDeclaredFields()){ + Object value = null; + try { + f.setAccessible(Boolean.TRUE); + value = f.get(this); + details.put(f.getName(), value); + } catch (Exception e) { + } + + } + } } diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/Event.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/Event.java deleted file mode 100644 index 29dd3a36c..000000000 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/Event.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.lowcoder.infra.event; - -public interface Event { - - EventType getEventType(); -} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/EventType.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/EventType.java deleted file mode 100644 index 52260736f..000000000 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/EventType.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.lowcoder.infra.event; - -import java.util.Locale; - -import org.lowcoder.sdk.util.LocaleUtils; - -public enum EventType { - - USER_LOGIN("EVENT_TYPE_USER_LOGIN"), - USER_LOGOUT("EVENT_TYPE_USER_LOGOUT"), - - // application - VIEW("EVENT_TYPE_VIEW"), - APPLICATION_CREATE("EVENT_TYPE_APPLICATION_CREATE"), - APPLICATION_DELETE("EVENT_TYPE_APPLICATION_DELETE"), - APPLICATION_UPDATE("EVENT_TYPE_APPLICATION_UPDATE"), - APPLICATION_MOVE("EVENT_TYPE_APPLICATION_MOVE"), - APPLICATION_RECYCLED("EVENT_TYPE_APPLICATION_RECYCLED"), - APPLICATION_RESTORE("EVENT_TYPE_APPLICATION_RESTORE"), - - // folder - FOLDER_CREATE("EVENT_TYPE_FOLDER_CREATE"), - FOLDER_DELETE("EVENT_TYPE_FOLDER_DELETE"), - FOLDER_UPDATE("EVENT_TYPE_FOLDER_UPDATE"), - - // query - QUERY_EXECUTION("EVENT_TYPE_QUERY_EXECUTION"), - // group - GROUP_CREATE("EVENT_TYPE_GROUP_CREATE"), - GROUP_UPDATE("EVENT_TYPE_GROUP_UPDATE"), - GROUP_DELETE("EVENT_TYPE_GROUP_DELETE"), - GROUP_MEMBER_ADD("EVENT_TYPE_GROUP_MEMBER_ADD"), - GROUP_MEMBER_ROLE_UPDATE("EVENT_TYPE_GROUP_MEMBER_ROLE_UPDATE"), - GROUP_MEMBER_LEAVE("EVENT_TYPE_GROUP_MEMBER_LEAVE"), - GROUP_MEMBER_REMOVE("EVENT_TYPE_GROUP_MEMBER_REMOVE"), - //system - SERVER_START_UP("EVENT_TYPE_SERVER_START_UP"), - - // data source - DATA_SOURCE_CREATE("DATA_SOURCE_CREATE"), - DATA_SOURCE_UPDATE("DATA_SOURCE_UPDATE"), - DATA_SOURCE_DELETE("DATA_SOURCE_DELETE"), - DATA_SOURCE_PERMISSION_GRANT("DATA_SOURCE_PERMISSION_GRANT"), - DATA_SOURCE_PERMISSION_UPDATE("DATA_SOURCE_PERMISSION_UPDATE"), - DATA_SOURCE_PERMISSION_DELETE("DATA_SOURCE_PERMISSION_DELETE"), - - // library query - LIBRARY_QUERY_CREATE("LIBRARY_QUERY_CREATE"), - LIBRARY_QUERY_UPDATE("LIBRARY_QUERY_UPDATE"), - LIBRARY_QUERY_DELETE("LIBRARY_QUERY_DELETE"), - LIBRARY_QUERY_PUBLISH("LIBRARY_QUERY_PUBLISH"), - ; - - private final String desc; - - EventType(String desc) { - this.desc = desc; - } - - public String getDesc(Locale locale) { - return LocaleUtils.getMessage(locale, this.desc); - } -} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/SystemCommonEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/SystemCommonEvent.java new file mode 100644 index 000000000..5ddacf5c1 --- /dev/null +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/SystemCommonEvent.java @@ -0,0 +1,18 @@ +package org.lowcoder.infra.event; + +import org.checkerframework.checker.units.qual.C; + +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class SystemCommonEvent extends AbstractEvent +{ + private final long apiCalls; + + @Override + public EventType getEventType() { + return EventType.SERVER_INFO; + } +} diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java index 7c724b68d..4c5471d68 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourceEvent.java @@ -1,7 +1,6 @@ package org.lowcoder.infra.event.datasource; import org.lowcoder.infra.event.AbstractEvent; -import org.lowcoder.infra.event.EventType; import lombok.Getter; import lombok.experimental.SuperBuilder; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java index 9e967e248..99d2703cb 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/datasource/DatasourcePermissionEvent.java @@ -3,7 +3,6 @@ import java.util.Collection; import org.lowcoder.infra.event.AbstractEvent; -import org.lowcoder.infra.event.EventType; import lombok.Getter; import lombok.experimental.SuperBuilder; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java index d2983a29c..ab80e0cc0 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupCreateEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.group; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java index 4da2b51e3..2d7caa495 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupDeleteEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.group; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java index ac6ef697d..9d06c459a 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/group/GroupUpdateEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.group; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java index bf5bcd89f..52c17df48 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberAddEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java index bd43fa482..d35db5198 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberLeaveEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java index 888da0aff..6b4fef1d2 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRemoveEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java index 62ea39478..785a28fc5 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/groupmember/GroupMemberRoleUpdateEvent.java @@ -1,7 +1,5 @@ package org.lowcoder.infra.event.groupmember; -import org.lowcoder.infra.event.EventType; - import lombok.experimental.SuperBuilder; @SuperBuilder diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java index c0e7fafd2..aa840de74 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLoginEvent.java @@ -1,7 +1,6 @@ package org.lowcoder.infra.event.user; import org.lowcoder.infra.event.AbstractEvent; -import org.lowcoder.infra.event.EventType; import lombok.Getter; import lombok.experimental.SuperBuilder; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java index 8e0a8b073..cf2fdd714 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/user/UserLogoutEvent.java @@ -1,7 +1,6 @@ package org.lowcoder.infra.event.user; import org.lowcoder.infra.event.AbstractEvent; -import org.lowcoder.infra.event.EventType; import lombok.experimental.SuperBuilder; diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java index 0eb36e585..f50939f94 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/localcache/ReloadableCache.java @@ -83,7 +83,7 @@ public ReloadableCache build() { private void startScheduledReloadTask(ReloadableCache cache) { ScheduledExecutorService scheduledExecutor = newSingleThreadScheduledExecutor(); scheduledExecutor.scheduleAtFixedRate(() -> { - log.debug("{} scheduled reload...", cacheName); + log.trace("{} scheduled reload...", cacheName); try { cache.cachedValue = factory.getValue().block(); } catch (Exception e) { diff --git a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java index b45708a20..6faf54dc7 100644 --- a/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java +++ b/server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/serverlog/ServerLogService.java @@ -10,8 +10,10 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.collections4.CollectionUtils; +import org.lowcoder.infra.event.SystemCommonEvent; import org.lowcoder.infra.perf.PerfHelper; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -27,6 +29,9 @@ public class ServerLogService { @Autowired private PerfHelper perfHelper; + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + private volatile Queue serverLogs = new ConcurrentLinkedQueue<>(); public void record(ServerLog serverLog) { @@ -43,7 +48,13 @@ private void scheduledInsert() { serverLogRepository.saveAll(tmp) .collectList() .subscribe(result -> { + int count = result.size(); perfHelper.count(SERVER_LOG_BATCH_INSERT, Tags.of("size", String.valueOf(result.size()))); + applicationEventPublisher.publishEvent(SystemCommonEvent.builder() + .apiCalls(count) + .detail("apiCalls", Integer.toString(count)) + .build() + ); }); } diff --git a/server/api-service/lowcoder-plugins/clickHousePlugin/plugin.properties b/server/api-service/lowcoder-plugins/clickHousePlugin/plugin.properties deleted file mode 100644 index 822e4fa85..000000000 --- a/server/api-service/lowcoder-plugins/clickHousePlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=clickHouse-plugin -plugin.class=org.lowcoder.plugin.clickhouse.ClickHousePlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/elasticSearchPlugin/plugin.properties b/server/api-service/lowcoder-plugins/elasticSearchPlugin/plugin.properties deleted file mode 100644 index 87717ad57..000000000 --- a/server/api-service/lowcoder-plugins/elasticSearchPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=es-plugin -plugin.class=org.lowcoder.plugin.es.EsPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/elasticSearchPlugin/src/main/java/org/lowcoder/plugin/es/ReactorRestClientAdaptor.java b/server/api-service/lowcoder-plugins/elasticSearchPlugin/src/main/java/org/lowcoder/plugin/es/ReactorRestClientAdaptor.java index f8fb278d1..1bc1efebb 100644 --- a/server/api-service/lowcoder-plugins/elasticSearchPlugin/src/main/java/org/lowcoder/plugin/es/ReactorRestClientAdaptor.java +++ b/server/api-service/lowcoder-plugins/elasticSearchPlugin/src/main/java/org/lowcoder/plugin/es/ReactorRestClientAdaptor.java @@ -3,7 +3,7 @@ import java.io.Closeable; import java.io.IOException; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; diff --git a/server/api-service/lowcoder-plugins/elasticSearchPlugin/src/main/java/org/lowcoder/plugin/es/model/EsConnection.java b/server/api-service/lowcoder-plugins/elasticSearchPlugin/src/main/java/org/lowcoder/plugin/es/model/EsConnection.java index 713925ae5..c68b2c5e6 100644 --- a/server/api-service/lowcoder-plugins/elasticSearchPlugin/src/main/java/org/lowcoder/plugin/es/model/EsConnection.java +++ b/server/api-service/lowcoder-plugins/elasticSearchPlugin/src/main/java/org/lowcoder/plugin/es/model/EsConnection.java @@ -3,7 +3,7 @@ import java.io.Closeable; import java.io.IOException; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import org.lowcoder.plugin.es.ReactorRestClientAdaptor; diff --git a/server/api-service/lowcoder-plugins/googleSheetsPlugin/plugin.properties b/server/api-service/lowcoder-plugins/googleSheetsPlugin/plugin.properties deleted file mode 100644 index 7c9cd8c66..000000000 --- a/server/api-service/lowcoder-plugins/googleSheetsPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=googleSheets-plugin -plugin.class=org.lowcoder.plugin.googlesheets.GoogleSheetsPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/graphqlPlugin/plugin.properties b/server/api-service/lowcoder-plugins/graphqlPlugin/plugin.properties deleted file mode 100644 index 5d4dd5bba..000000000 --- a/server/api-service/lowcoder-plugins/graphqlPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=graphql-plugin -plugin.class=org.lowcoder.plugin.graphql.GraphQLPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/lowcoderApiPlugin/plugin.properties b/server/api-service/lowcoder-plugins/lowcoderApiPlugin/plugin.properties deleted file mode 100644 index 545de1ba2..000000000 --- a/server/api-service/lowcoder-plugins/lowcoderApiPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=lowcoder-api-plugin -plugin.class=org.lowcoder.plugin.LowcoderApiPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/mongoPlugin/plugin.properties b/server/api-service/lowcoder-plugins/mongoPlugin/plugin.properties deleted file mode 100644 index a18bf7f80..000000000 --- a/server/api-service/lowcoder-plugins/mongoPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=mongo-plugin -plugin.class=org.lowcoder.plugin.mongo.MongoPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/mssqlPlugin/plugin.properties b/server/api-service/lowcoder-plugins/mssqlPlugin/plugin.properties deleted file mode 100644 index 002e43851..000000000 --- a/server/api-service/lowcoder-plugins/mssqlPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=mssql-plugin -plugin.class=org.lowcoder.plugin.mssql.MssqlPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/mysqlPlugin/plugin.properties b/server/api-service/lowcoder-plugins/mysqlPlugin/plugin.properties deleted file mode 100644 index 2e2c88008..000000000 --- a/server/api-service/lowcoder-plugins/mysqlPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=mysql-plugin -plugin.class=org.lowcoder.plugin.mysql.MysqlPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/oraclePlugin/plugin.properties b/server/api-service/lowcoder-plugins/oraclePlugin/plugin.properties deleted file mode 100644 index 516f2de00..000000000 --- a/server/api-service/lowcoder-plugins/oraclePlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=oracle-plugin -plugin.class=org.lowcoder.plugin.oracle.OraclePlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml b/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml index fcd91b289..67eb51702 100644 --- a/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml +++ b/server/api-service/lowcoder-plugins/oraclePlugin/pom.xml @@ -13,6 +13,9 @@ + UTF-8 + UTF-8 + 17 17 diff --git a/server/api-service/lowcoder-plugins/pom.xml b/server/api-service/lowcoder-plugins/pom.xml index 11807e458..90512a3f5 100644 --- a/server/api-service/lowcoder-plugins/pom.xml +++ b/server/api-service/lowcoder-plugins/pom.xml @@ -79,6 +79,14 @@ + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + org.lowcoder sqlBasedPlugin diff --git a/server/api-service/lowcoder-plugins/postgresPlugin/plugin.properties b/server/api-service/lowcoder-plugins/postgresPlugin/plugin.properties deleted file mode 100644 index bbd887fb0..000000000 --- a/server/api-service/lowcoder-plugins/postgresPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=postgres-plugin -plugin.class=org.lowcoder.plugin.postgres.PostgresPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/redisPlugin/plugin.properties b/server/api-service/lowcoder-plugins/redisPlugin/plugin.properties deleted file mode 100644 index ded41c272..000000000 --- a/server/api-service/lowcoder-plugins/redisPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=redis-plugin -plugin.class=org.lowcoder.plugin.redis.RedisPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/restApiPlugin/plugin.properties b/server/api-service/lowcoder-plugins/restApiPlugin/plugin.properties deleted file mode 100644 index 0ed0b7d87..000000000 --- a/server/api-service/lowcoder-plugins/restApiPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=restapi-plugin -plugin.class=org.lowcoder.plugin.restapi.RestApiPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/smtpPlugin/plugin.properties b/server/api-service/lowcoder-plugins/smtpPlugin/plugin.properties deleted file mode 100644 index 70d475de9..000000000 --- a/server/api-service/lowcoder-plugins/smtpPlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=smtp-plugin -plugin.class=org.lowcoder.plugins.SmtpPlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-plugins/smtpPlugin/pom.xml b/server/api-service/lowcoder-plugins/smtpPlugin/pom.xml index ca7b0516a..fdf945b2b 100644 --- a/server/api-service/lowcoder-plugins/smtpPlugin/pom.xml +++ b/server/api-service/lowcoder-plugins/smtpPlugin/pom.xml @@ -27,9 +27,9 @@ - javax.mail - mail - 1.5.0-b01 + jakarta.mail + jakarta.mail-api + 2.1.3 diff --git a/server/api-service/lowcoder-plugins/smtpPlugin/src/main/java/org/lowcoder/plugins/SmtpPlugin.java b/server/api-service/lowcoder-plugins/smtpPlugin/src/main/java/org/lowcoder/plugins/SmtpPlugin.java index 186474174..bb463d445 100644 --- a/server/api-service/lowcoder-plugins/smtpPlugin/src/main/java/org/lowcoder/plugins/SmtpPlugin.java +++ b/server/api-service/lowcoder-plugins/smtpPlugin/src/main/java/org/lowcoder/plugins/SmtpPlugin.java @@ -1,8 +1,8 @@ package org.lowcoder.plugins; -import static javax.mail.Message.RecipientType.BCC; -import static javax.mail.Message.RecipientType.CC; -import static javax.mail.Message.RecipientType.TO; +import static jakarta.mail.Message.RecipientType.BCC; +import static jakarta.mail.Message.RecipientType.CC; +import static jakarta.mail.Message.RecipientType.TO; import static org.apache.commons.collections4.MapUtils.getString; import static org.apache.commons.lang3.ArrayUtils.isNotEmpty; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -27,22 +27,22 @@ import java.util.Set; import java.util.stream.Stream; -import javax.activation.DataHandler; -import javax.annotation.Nonnull; -import javax.mail.Authenticator; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.Multipart; -import javax.mail.Part; -import javax.mail.PasswordAuthentication; -import javax.mail.Session; -import javax.mail.Transport; -import javax.mail.internet.AddressException; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeBodyPart; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMultipart; -import javax.mail.util.ByteArrayDataSource; +import jakarta.activation.DataHandler; +import jakarta.annotation.Nonnull; +import jakarta.mail.Authenticator; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.Part; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.ByteArrayDataSource; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; diff --git a/server/api-service/lowcoder-plugins/smtpPlugin/src/main/java/org/lowcoder/plugins/SmtpQueryExecutionContext.java b/server/api-service/lowcoder-plugins/smtpPlugin/src/main/java/org/lowcoder/plugins/SmtpQueryExecutionContext.java index 6000d4f52..835afdccd 100644 --- a/server/api-service/lowcoder-plugins/smtpPlugin/src/main/java/org/lowcoder/plugins/SmtpQueryExecutionContext.java +++ b/server/api-service/lowcoder-plugins/smtpPlugin/src/main/java/org/lowcoder/plugins/SmtpQueryExecutionContext.java @@ -2,7 +2,7 @@ import java.util.List; -import javax.mail.internet.InternetAddress; +import jakarta.mail.internet.InternetAddress; import org.lowcoder.sdk.query.QueryExecutionContext; diff --git a/server/api-service/lowcoder-plugins/snowflakePlugin/plugin.properties b/server/api-service/lowcoder-plugins/snowflakePlugin/plugin.properties deleted file mode 100644 index 5f7dbca58..000000000 --- a/server/api-service/lowcoder-plugins/snowflakePlugin/plugin.properties +++ /dev/null @@ -1,5 +0,0 @@ -plugin.id=snowflake-plugin -plugin.class=org.lowcoder.plugin.snowflake.SnowflakePlugin -plugin.version=2.0.1-SNAPSHOT -plugin.provider=service@lowcoder.org -plugin.dependencies= \ No newline at end of file diff --git a/server/api-service/lowcoder-sdk/pom.xml b/server/api-service/lowcoder-sdk/pom.xml index cbd69d47c..63976f6f2 100644 --- a/server/api-service/lowcoder-sdk/pom.xml +++ b/server/api-service/lowcoder-sdk/pom.xml @@ -5,7 +5,7 @@ lowcoder-root org.lowcoder - ${revision} + ${revision} 4.0.0 @@ -13,11 +13,6 @@ lowcoder-sdk - - UTF-8 - 17 - - org.springframework.boot @@ -95,9 +90,9 @@ org.hibernate.validator hibernate-validator - - javax.el - javax.el-api + + jakarta.el + jakarta.el-api net.minidev @@ -166,9 +161,32 @@ jakarta.annotation jakarta.annotation-api - - javax.validation - validation-api + + jakarta.validation + jakarta.validation-api + + + UTF-8 + UTF-8 + + 17 + + 17 + 17 + + + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + + diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java index d1fcf3ea8..b6fed12ee 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java @@ -44,7 +44,11 @@ public class CommonConfig { private Cookie cookie = new Cookie(); private JsExecutor jsExecutor = new JsExecutor(); private Set disallowedHosts = new HashSet<>(); + private List pluginDirs = new ArrayList<>(); + private SuperAdmin superAdmin = new SuperAdmin(); private Marketplace marketplace = new Marketplace(); + private String lowcoderPublicUrl; + private String lostPasswordEmailSender; public boolean isSelfHost() { return !isCloud(); @@ -158,4 +162,10 @@ public static class Marketplace { public static class Query { private long readStructureTimeout = 15000; } + + @Data + public static class SuperAdmin { + private String userName; + private String password; + } } diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/MustacheHelper.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/MustacheHelper.java index a6aa48c76..c1e65c6b7 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/MustacheHelper.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/MustacheHelper.java @@ -42,8 +42,8 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import javax.annotation.Nonnull; -import javax.validation.constraints.NotNull; +import jakarta.annotation.Nonnull; +import jakarta.validation.constraints.NotNull; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.Range; diff --git a/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties b/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties index d81ecbdf2..1e3a18c31 100644 --- a/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties +++ b/server/api-service/lowcoder-sdk/src/main/resources/locale_en.properties @@ -17,6 +17,9 @@ CANNOT_DELETE_SYSTEM_GROUP=System group cannot be deleted. NEED_DEV_TO_CREATE_RESOURCE=Invalid operation, workspace developers or admin required. UNABLE_TO_FIND_VALID_ORG=Cannot find a valid workspace for current user. USER_BANNED=Current account is frozen. +SENDING_EMAIL_FAILED=Email could not be sent. Please check the smtp settings for the org. +TOKEN_EXPIRED=Token to reset the password has expired +INVALID_TOKEN=Invalid token received for password reset request # invitation INVALID_INVITATION_CODE=Invitation code not found. ALREADY_IN_ORGANIZATION=You are already in this workspace. diff --git a/server/api-service/lowcoder-server/cert/README b/server/api-service/lowcoder-server/cert/README new file mode 100644 index 000000000..0589816e8 --- /dev/null +++ b/server/api-service/lowcoder-server/cert/README @@ -0,0 +1,33 @@ +To generate the signing keys in PKCS#12 format: + +$ keytool -genkey -alias dev -keyalg RSA -keysize 4096 -validity 36500 -keystore signing.p12 -storetype pkcs12 + +Enter keystore password: +Re-enter new password: +What is your first and last name? + [Unknown]: dev.lowcoder.org +What is the name of your organizational unit? + [Unknown]: dev +What is the name of your organization? + [Unknown]: Lowcoder Software LTD +What is the name of your City or Locality? + [Unknown]: London +What is the name of your State or Province? + [Unknown]: United Kingdom +What is the two-letter country code for this unit? + [Unknown]: UK +Is CN=dev.lowcoder.org, OU=dev, O=Lowcoder Software LTD, L=London, ST=United Kingdom, C=UK correct? + [no]: yes + +Generating 4,096 bit RSA key pair and self-signed certificate (SHA384withRSA) with a validity of 36,500 days + for: CN=dev.lowcoder.org, OU=dev, O=Lowcoder Software LTD, L=London, ST=United Kingdom, C=UK + + + +To export the public key from generated key pair: + +$ openssl rsa -in signing.p12 -pubout -out lowcoder.pub + +Enter pass phrase for PKCS12 import pass phrase: +writing RSA key + diff --git a/server/api-service/lowcoder-server/cert/signing.p12 b/server/api-service/lowcoder-server/cert/signing.p12 new file mode 100644 index 000000000..2f336a1f6 Binary files /dev/null and b/server/api-service/lowcoder-server/cert/signing.p12 differ diff --git a/server/api-service/lowcoder-server/pom.xml b/server/api-service/lowcoder-server/pom.xml index cd2d2ed86..5021d2b61 100644 --- a/server/api-service/lowcoder-server/pom.xml +++ b/server/api-service/lowcoder-server/pom.xml @@ -1,281 +1,380 @@ - - 4.0.0 - - lowcoder-root - org.lowcoder - ${revision} - + + 4.0.0 + + lowcoder-root + org.lowcoder + ${revision} + - lowcoder-server - jar + lowcoder-server + jar - lowcoder-server + lowcoder-server - - 17 - false - ${skipTests} - ${skipTests} - + + UTF-8 + UTF-8 - + 17 - - org.lowcoder - lowcoder-sdk - - - org.lowcoder - lowcoder-infra - - - org.lowcoder - lowcoder-domain - + false + ${skipTests} + ${skipTests} - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security - spring-security-config - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springdoc - springdoc-openapi-starter-webflux-ui - 2.2.0 - - - io.projectreactor.tools - blockhound - - - org.springframework.boot - spring-boot-starter-data-mongodb-reactive - + cert/signing.p12 + pkcs12 + dev + lowcoder + ${keystore.password} + ${keystore.password} + - - org.springframework.boot - spring-boot-starter-data-redis-reactive - + - - org.projectlombok - lombok - + + org.lowcoder + lowcoder-sdk + + + org.lowcoder + lowcoder-infra + + + org.lowcoder + lowcoder-domain + + + org.lowcoder.plugin + lowcoder-plugin-api + - com.google.guava - guava - - - commons-io - commons-io - - - org.springframework.boot - spring-boot-starter-actuator - - - io.micrometer - micrometer-registry-prometheus - - - io.sentry - sentry-spring-boot-starter - - - org.apache.httpcomponents - httpclient - - - org.apache.commons - commons-text - - - - org.apache.commons - commons-collections4 - - + + org.apache.commons + commons-collections4 + + + - - io.netty - netty-all - runtime - - - io.projectreactor - reactor-tools - - - org.mockito - mockito-inline - test - - - org.mockito - mockito-core - test - - - junit - junit - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - io.projectreactor - reactor-test - test - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo.spring30x - test - - - com.jayway.jsonpath - json-path - - - jakarta.servlet - jakarta.servlet-api - + + io.netty + netty-all + runtime + + + io.projectreactor + reactor-tools + + + org.mockito + mockito-inline + test + + + org.mockito + mockito-core + test + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + io.projectreactor + reactor-test + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo.spring30x + test + + + com.jayway.jsonpath + json-path + + + jakarta.servlet + jakarta.servlet-api + - - com.auth0 - java-jwt - 4.4.0 - + + com.auth0 + java-jwt + 4.4.0 + - - it.ozimov - embedded-redis - 0.7.3 - test - - - org.apache.directory.server - apacheds-test-framework - 2.0.0.AM26 - test - - - org.junit.vintage - junit-vintage-engine - 5.9.3 - test - - - io.jsonwebtoken - jjwt-api - 0.11.5 - compile - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - compile - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - + + org.passay + passay + 1.6.3 + + + + it.ozimov + embedded-redis + 0.7.3 + test + + + org.apache.directory.server + apacheds-test-framework + 2.0.0.AM26 + test + + + org.junit.vintage + junit-vintage-engine + 5.9.3 + test + + + io.jsonwebtoken + jjwt-api + 0.11.5 + compile + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + compile + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + org.springframework + spring-aspects + + + org.springframework + spring-aspects + + + + + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + - + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + -parameters + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.lowcoder.api.ServerApplication + true + true + true + + + + + + org.apache.maven.plugins + maven-jarsigner-plugin + 3.0.0 + + + sign + + sign + + + + verify + + verify + + + + + ${keystore.type} + ${keystore.path} + ${keystore.alias} + ${keystore.store.password} + ${keystore.key.password} + + - - - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - 3.1.2 - - ${skipUnitTests} - - **/*IntegrationTest.java - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - ${skipIntegrationTests} - - **/*IntegrationTest.java - - - -Dpf4j.pluginsDir=../lowcoder-plugins/plugins - - - - - - integration-test - verify - - - - - - maven-antrun-plugin - - - copy-plugins-jar-for-integration-tests - pre-integration-test - - - - - - - - - - run - - - - delete-plugins-after-integration-tests-phase - post-integration-test - - - - - - - run - - - - - - + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + ${skipUnitTests} + + **/*IntegrationTest.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${skipIntegrationTests} + + **/*IntegrationTest.java + + + -Dpf4j.pluginsDir=../lowcoder-plugins/plugins + + + + + + integration-test + verify + + + + + + maven-antrun-plugin + + + copy-plugins-jar-for-integration-tests + pre-integration-test + + + + + + + + + + run + + + + delete-plugins-after-integration-tests-phase + post-integration-test + + + + + + + run + + + + + + diff --git a/server/api-service/lowcoder-server/src/main/assembly/assembly.xml b/server/api-service/lowcoder-server/src/main/assembly/assembly.xml new file mode 100644 index 000000000..b2f6bb420 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/assembly/assembly.xml @@ -0,0 +1,58 @@ + + + lowcoder-dist + + dir + + + true + lowcoder + + + + target/${project.artifactId}-${project.version}.jar + + application.jar + + + + + + + \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java index 3a442255b..09c94ee06 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java @@ -45,6 +45,9 @@ public void init() { public static void main(String[] args) { + /** Disable Java Flight Recorder for Redis Lettuce driver **/ + System.setProperty("io.lettuce.core.jfr", "false"); + Schedulers.enableMetrics(); new SpringApplicationBuilder(ServerApplication.class) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java index d12297b33..de398e01f 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java @@ -1,11 +1,12 @@ package org.lowcoder.api.application; import static org.apache.commons.collections4.SetUtils.emptyIfNull; -import static org.lowcoder.infra.event.EventType.APPLICATION_CREATE; -import static org.lowcoder.infra.event.EventType.APPLICATION_DELETE; -import static org.lowcoder.infra.event.EventType.APPLICATION_RECYCLED; -import static org.lowcoder.infra.event.EventType.APPLICATION_RESTORE; -import static org.lowcoder.infra.event.EventType.APPLICATION_UPDATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_CREATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_DELETE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_RECYCLED; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_RESTORE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_UPDATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_VIEW; import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; @@ -26,7 +27,6 @@ import org.lowcoder.domain.application.model.ApplicationStatus; import org.lowcoder.domain.application.model.ApplicationType; import org.lowcoder.domain.permission.model.ResourceRole; -import org.lowcoder.infra.event.EventType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -93,12 +93,11 @@ public Mono> getEditingApplication(@PathVariable S .map(ResponseView::success); } - // will call the check in ApplicationApiService and ApplicationService @Override public Mono> getPublishedApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_ALL) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW)) .map(ResponseView::success); } @@ -106,7 +105,7 @@ public Mono> getPublishedApplication(@PathVariable public Mono> getPublishedMarketPlaceApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_MARKETPLACE) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW)) .map(ResponseView::success); } @@ -114,7 +113,7 @@ public Mono> getPublishedMarketPlaceApplication(@P public Mono> getAgencyProfileApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.AGENCY_PROFILE) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW)) .map(ResponseView::success); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java index 166801e7d..c28740cdc 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java @@ -149,7 +149,7 @@ public Mono loginOrRegister(AuthUser authUser, ServerWebExchange exchange, boolean createWorkspace = authUser.getOrgId() == null && StringUtils.isBlank(invitationId) && authProperties.getWorkspaceCreation(); if (user.getIsNewUser() && createWorkspace) { - return onUserRegister(user); + return onUserRegister(user, false); } return Mono.empty(); }) @@ -166,7 +166,7 @@ public Mono loginOrRegister(AuthUser authUser, ServerWebExchange exchange, .then(businessEventPublisher.publishUserLoginEvent(authUser.getSource())); } - private Mono updateOrCreateUser(AuthUser authUser, boolean linkExistingUser) { + public Mono updateOrCreateUser(AuthUser authUser, boolean linkExistingUser) { if(linkExistingUser) { return sessionUserService.getVisitor() @@ -256,8 +256,8 @@ protected Connection getAuthConnection(AuthUser authUser, User user) { .get(); } - protected Mono onUserRegister(User user) { - return organizationService.createDefault(user).then(); + public Mono onUserRegister(User user, boolean isSuperAdmin) { + return organizationService.createDefault(user, isSuperAdmin).then(); } protected Mono onUserLogin(String orgId, User user, String source) { @@ -362,7 +362,7 @@ private Mono removeTokensByAuthId(String authId) { private Mono checkIfAdmin() { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.empty(); } return deferredError(BizError.NOT_AUTHORIZED, "NOT_AUTHORIZED"); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java index 1494f7786..4155a2b7f 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java @@ -1,11 +1,11 @@ package org.lowcoder.api.datasource; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_CREATE; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_DELETE; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_PERMISSION_DELETE; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_PERMISSION_GRANT; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_PERMISSION_UPDATE; -import static org.lowcoder.infra.event.EventType.DATA_SOURCE_UPDATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_CREATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_DELETE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_PERMISSION_DELETE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_PERMISSION_GRANT; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_PERMISSION_UPDATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.DATA_SOURCE_UPDATE; import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; import static org.lowcoder.sdk.util.LocaleUtils.getLocale; @@ -14,7 +14,7 @@ import java.util.List; import java.util.Locale; -import javax.validation.Valid; +import jakarta.validation.Valid; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java index 94307b625..69a7968fe 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java @@ -3,8 +3,8 @@ import java.util.List; import java.util.Set; -import javax.annotation.Nullable; -import javax.validation.Valid; +import jakarta.annotation.Nullable; +import jakarta.validation.Valid; import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.api.permission.view.CommonPermissionView; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java index 1170b9761..763dccd7c 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/ApplicationConfiguration.java @@ -1,7 +1,10 @@ package org.lowcoder.api.framework.configuration; +import org.lowcoder.api.ServerApplication; import org.lowcoder.sdk.config.CommonConfig; +import org.pf4j.spring.SpringPluginManager; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.system.ApplicationHome; import org.springframework.boot.web.servlet.MultipartConfigFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,6 +18,18 @@ public class ApplicationConfiguration @Autowired private CommonConfig common; + @Bean("applicationHome") + public ApplicationHome applicatioHome() + { + return new ApplicationHome(ServerApplication.class); + } + + @Bean + public SpringPluginManager pluginManager() + { + return new SpringPluginManager(); + } + @Bean public MultipartConfigElement multipartConfigElement() { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/CustomWebFluxConfigurationSupport.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/CustomWebFluxConfigurationSupport.java new file mode 100644 index 000000000..d57b0ab1d --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/CustomWebFluxConfigurationSupport.java @@ -0,0 +1,16 @@ +package org.lowcoder.api.framework.configuration; + +import org.lowcoder.api.framework.plugin.endpoint.ReloadableRouterFunctionMapping; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.WebFluxConfigurationSupport; +import org.springframework.web.reactive.function.server.support.RouterFunctionMapping; + +@Configuration +public class CustomWebFluxConfigurationSupport extends WebFluxConfigurationSupport +{ + @Override + protected RouterFunctionMapping createRouterFunctionMapping() + { + return new ReloadableRouterFunctionMapping(); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java new file mode 100644 index 000000000..c7feaae0f --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java @@ -0,0 +1,57 @@ +package org.lowcoder.api.framework.configuration; + +import java.util.ArrayList; + +import org.lowcoder.api.framework.plugin.LowcoderPluginManager; +import org.lowcoder.api.framework.plugin.endpoint.PluginEndpointHandler; +// Falk: eventually not needed +import org.lowcoder.api.framework.plugin.security.PluginAuthorizationManager; +import org.lowcoder.plugin.api.EndpointExtension; +import org.springframework.aop.Advisor; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Role; +import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeReactiveMethodInterceptor; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import reactor.core.publisher.Mono; + +@Configuration +public class PluginConfiguration +{ + + @SuppressWarnings("unchecked") + @Bean + @DependsOn("lowcoderPluginManager") + RouterFunction pluginEndpoints(LowcoderPluginManager pluginManager, PluginEndpointHandler pluginEndpointHandler) + { + RouterFunction pluginsList = RouterFunctions.route() + .GET(RequestPredicates.path(PluginEndpointHandler.PLUGINS_BASE_URL), req -> ServerResponse.ok().body(Mono.just(pluginManager.getLoadedPluginsInfo()), ArrayList.class)) + .build(); + + RouterFunction endpoints = pluginEndpointHandler.registeredEndpoints().stream() + .map(r-> (RouterFunction)r) + .reduce((o, r )-> (RouterFunction) o.andOther(r)) + .orElse(null); + + return (endpoints == null) ? pluginsList : pluginsList.andOther(endpoints); + } + + // Falk: eventually not needed + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor protectPluginEndpoints(PluginAuthorizationManager pluginAauthManager) + { + AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(EndpointExtension.class, true); + AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(pointcut, pluginAauthManager); + interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder() -1); + return interceptor; + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIDelayFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIDelayFilter.java new file mode 100644 index 000000000..6f45c7e7c --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIDelayFilter.java @@ -0,0 +1,38 @@ +package org.lowcoder.api.framework.filter; + +import org.lowcoder.infra.config.repository.ServerConfigRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +import static org.lowcoder.api.framework.filter.FilterOrder.API_DELAY_FILTER; + +@Component +public class APIDelayFilter implements WebFilter, Ordered { + + @Autowired + private ServerConfigRepository serverConfigRepository; + + @Override + public int getOrder() { + return API_DELAY_FILTER.getOrder(); + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return serverConfigRepository.findByKey("isRateLimited") + .map(serverConfig -> { + if(serverConfig.getValue() != null && Boolean.parseBoolean(serverConfig.getValue().toString())) { + return Mono.delay(Duration.ofSeconds(5)).block(); + } else { + return Mono.empty(); + } + }).then(chain.filter(exchange)); + } +} \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java index 8e8c0d9be..9bf6b4100 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/FilterOrder.java @@ -10,6 +10,8 @@ public enum FilterOrder { REQUEST_COST(BEFORE_PROXY_CHAIN), THROTTLING(BEFORE_PROXY_CHAIN), + API_DELAY_FILTER(BEFORE_PROXY_CHAIN), + // WEB_FILTER_CHAIN_PROXY here USER_BAN(AFTER_PROXY_CHAIN), diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextFilter.java new file mode 100644 index 000000000..e8c2fb765 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextFilter.java @@ -0,0 +1,18 @@ +package org.lowcoder.api.framework.filter; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +@Configuration +public class ReactiveRequestContextFilter implements WebFilter { + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + return chain.filter(exchange) + .contextWrite(ctx -> ctx.put(ReactiveRequestContextHolder.SERVER_HTTP_REQUEST, request)); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextHolder.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextHolder.java new file mode 100644 index 000000000..98477a012 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextHolder.java @@ -0,0 +1,13 @@ +package org.lowcoder.api.framework.filter; + +import org.springframework.http.server.reactive.ServerHttpRequest; +import reactor.core.publisher.Mono; + +public class ReactiveRequestContextHolder { + public static final Class SERVER_HTTP_REQUEST = ServerHttpRequest.class; + + public static Mono getRequest() { + return Mono.subscriberContext() + .map(ctx -> ctx.get(SERVER_HTTP_REQUEST)); + } +} \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java index e3e8ba138..edbf45c9f 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ThrottlingFilter.java @@ -48,7 +48,7 @@ public class ThrottlingFilter implements WebFilter, Ordered { @PostConstruct private void init() { urlRateLimiter = configCenter.threshold().ofMap("urlRateLimiter", String.class, Integer.class, emptyMap()); - log.info("API rate limit filter enabled with default rate limit set to: {} requests per second"); + log.info("API rate limit filter enabled with default rate limit set to: {} requests per second", defaultApiRateLimit); } @Nonnull diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/LowcoderPluginManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/LowcoderPluginManager.java new file mode 100644 index 000000000..e4107919f --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/LowcoderPluginManager.java @@ -0,0 +1,130 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.lowcoder.plugin.api.LowcoderPlugin; +import org.lowcoder.plugin.api.LowcoderServices; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RequiredArgsConstructor +@Component +@Slf4j +public class LowcoderPluginManager +{ + private final LowcoderServices lowcoderServices; + private final PluginLoader pluginLoader; + private final Environment environment; + + private Map plugins = new LinkedHashMap<>(); + + @PostConstruct + private void loadPlugins() + { + registerPlugins(); + List sorted = new ArrayList<>(plugins.values()); + sorted.sort(Comparator.comparing(LowcoderPlugin::loadOrder)); + + for (LowcoderPlugin plugin : sorted) + { + PluginExecutor executor = new PluginExecutor(plugin, getPluginEnvironmentVariables(plugin), lowcoderServices); + executor.start(); + } + } + + @PreDestroy + public void unloadPlugins() + { + for (LowcoderPlugin plugin : plugins.values()) + { + try + { + plugin.unload(); + } + catch(Throwable cause) + { + log.warn("Error unloading plugin: {}!", plugin.pluginId(), cause); + } + } + } + + public List getLoadedPluginsInfo() + { + List infos = new ArrayList<>(); + for (LowcoderPlugin plugin : plugins.values()) + { + infos.add(new PluginInfo(plugin.pluginId(), plugin.description(), plugin.pluginInfo())); + } + return infos; + } + + private Map getPluginEnvironmentVariables(LowcoderPlugin plugin) + { + Map env = new HashMap<>(); + + String varPrefix = "PLUGIN_" + plugin.pluginId().toUpperCase().replaceAll("-", "_") + "_"; + MutablePropertySources propertySources = ((AbstractEnvironment) environment).getPropertySources(); + List properties = StreamSupport.stream(propertySources.spliterator(), false) + .filter(propertySource -> propertySource instanceof EnumerablePropertySource) + .map(propertySource -> ((EnumerablePropertySource) propertySource).getPropertyNames()) + .flatMap(Arrays:: stream) + .distinct() + .sorted() + .filter(prop -> prop.startsWith(varPrefix)) + .collect(Collectors.toList()); + + for (String prop : properties) + { + env.put(StringUtils.removeStart(prop, varPrefix), environment.getProperty(prop)); + } + + return env; + } + + private void registerPlugins() + { + List loaded = pluginLoader.loadPlugins(); + if (CollectionUtils.isNotEmpty(loaded)) + { + for (LowcoderPlugin plugin : loaded) + { + if (!plugins.containsKey(plugin.pluginId())) + { + log.info("Registered plugin: {} ({})", plugin.pluginId(), plugin.getClass().getName()); + plugins.put(plugin.pluginId(), plugin); + } + else + { + log.warn("Plugin {} already registered (from: {}), skipping {}.", plugin.pluginId(), + plugins.get(plugin.pluginId()).getClass().getName(), + plugin.getClass().getName()); + } + } + } + } + + private record PluginInfo( + String id, + String description, + Object info + ) {} + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java new file mode 100644 index 000000000..ddd66ba3f --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java @@ -0,0 +1,140 @@ +package org.lowcoder.api.framework.plugin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.lowcoder.plugin.api.LowcoderPlugin; +import org.lowcoder.sdk.config.CommonConfig; +import org.springframework.boot.system.ApplicationHome; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +@Component +public class PathBasedPluginLoader implements PluginLoader +{ + private final CommonConfig common; + private final ApplicationHome applicationHome; + + @Override + public List loadPlugins() + { + List plugins = new ArrayList<>(); + + List pluginJars = findPluginsJars(); + if (pluginJars.isEmpty()) + { + return plugins; + } + + for (String pluginJar : pluginJars) + { + log.debug("Inspecting plugin jar candidate: {}", pluginJar); + List loadedPlugins = loadPluginCandidates(pluginJar); + if (loadedPlugins.isEmpty()) + { + log.debug(" - no plugins found in the jar file"); + } + else + { + for (LowcoderPlugin plugin : loadedPlugins) + { + plugins.add(plugin); + } + } + } + + return plugins; + } + + protected List findPluginsJars() + { + List candidates = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(common.getPluginDirs())) + { + for (String pluginDir : common.getPluginDirs()) + { + final Path pluginPath = getAbsoluteNormalizedPath(pluginDir); + if (pluginPath != null) + { + candidates.addAll(findPluginCandidates(pluginPath)); + } + } + } + + return candidates; + } + + + protected List findPluginCandidates(Path pluginsDir) + { + List pluginCandidates = new ArrayList<>(); + try + { + Files.walk(pluginsDir) + .filter(Files::isRegularFile) + .filter(path -> StringUtils.endsWithIgnoreCase(path.toAbsolutePath().toString(), ".jar")) + .forEach(path -> pluginCandidates.add(path.toString())); + } + catch(IOException cause) + { + log.error("Error walking plugin folder! - {}", cause.getMessage()); + } + + return pluginCandidates; + } + + protected List loadPluginCandidates(String pluginJar) + { + List pluginCandidates = new ArrayList<>(); + + try + { + Path pluginPath = Path.of(pluginJar); + PluginClassLoader pluginClassLoader = new PluginClassLoader(pluginPath.getFileName().toString(), pluginPath); + + ServiceLoader pluginServices = ServiceLoader.load(LowcoderPlugin.class, pluginClassLoader); + if (pluginServices != null ) + { + Iterator pluginIterator = pluginServices.iterator(); + while(pluginIterator.hasNext()) + { + LowcoderPlugin plugin = pluginIterator.next(); + log.debug(" - loaded plugin: {} - {}", plugin.pluginId(), plugin.description()); + pluginCandidates.add(plugin); + } + } + } + catch(Throwable cause) + { + log.warn("Error loading plugin!", cause); + } + + return pluginCandidates; + } + + private Path getAbsoluteNormalizedPath(String path) + { + if (StringUtils.isNotBlank(path)) + { + Path absPath = Path.of(path); + if (!absPath.isAbsolute()) + { + absPath = Path.of(applicationHome.getDir().getAbsolutePath(), absPath.toString()); + } + return absPath.normalize().toAbsolutePath(); + } + + return null; + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java new file mode 100644 index 000000000..34945cdaf --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java @@ -0,0 +1,108 @@ +package org.lowcoder.api.framework.plugin; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; + +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +public class PluginClassLoader extends URLClassLoader +{ + private static final ClassLoader baseClassLoader = ClassLoader.getPlatformClassLoader(); + private final ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader(); + + private static final String[] excludedPaths = new String[] { + "org.lowcoder.plugin.api.", + "org/lowcoder/plugin/api/" + }; + + public PluginClassLoader(String name, Path pluginPath) + { + super(name, pathToURLs(pluginPath), baseClassLoader); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException + { + Class clazz = findLoadedClass(name); + if (clazz != null) + { + return clazz; + } + + if (StringUtils.startsWithAny(name, excludedPaths)) + { + try + { + clazz = appClassLoader.loadClass(name); + return clazz; + } + catch(Throwable cause) + { + log.error("[{}] :: Error loading class with appClassLoader - {}", name, cause.getMessage(), cause ); + } + } + + + try + { + clazz = super.loadClass(name, resolve); + if (clazz != null) + { + return clazz; + } + } + catch(NoClassDefFoundError cause) + { + log.error("[{}] :: Error loading class - {}", name, cause.getMessage(), cause ); + } + + return null; + } + + @Override + public URL getResource(String name) { + Objects.requireNonNull(name); + if (StringUtils.startsWithAny(name, excludedPaths)) + { + return appClassLoader.getResource(name); + } + return super.getResource(name); + } + + + @Override + public Enumeration getResources(String name) throws IOException + { + Objects.requireNonNull(name); + if (StringUtils.startsWithAny(name, excludedPaths)) + { + return appClassLoader.getResources(name); + } + return super.getResources(name); + } + + private static URL[] pathToURLs(Path path) + { + URL[] urls = null; + try + { + urls = new URL[] { path.toUri().toURL() }; + } + catch(MalformedURLException cause) + { + /** should not happen **/ + } + + return urls; + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java new file mode 100644 index 000000000..bbce19994 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java @@ -0,0 +1,36 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.Map; + +import org.lowcoder.plugin.api.LowcoderPlugin; +import org.lowcoder.plugin.api.LowcoderServices; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PluginExecutor extends Thread +{ + private Map env; + private LowcoderPlugin plugin; + private LowcoderServices services; + + public PluginExecutor(LowcoderPlugin plugin, Map env, LowcoderServices services) + { + this.env = env; + this.plugin = plugin; + this.services = services; + this.setContextClassLoader(plugin.getClass().getClassLoader()); + this.setName(plugin.pluginId()); + } + + @Override + public void run() + { + if (plugin.load(env, services)) + { + log.info("Plugin [{}] loaded and running.", plugin.pluginId()); + } + } + + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java new file mode 100644 index 000000000..25ed33eb4 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java @@ -0,0 +1,11 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.List; + +import org.lowcoder.plugin.api.LowcoderPlugin; + +public interface PluginLoader +{ + List loadPlugins(); + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java new file mode 100644 index 000000000..1cd455e20 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java @@ -0,0 +1,59 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; + +import org.lowcoder.api.framework.plugin.endpoint.PluginEndpointHandler; +import org.lowcoder.infra.config.repository.ServerConfigRepository; +import org.lowcoder.plugin.api.LowcoderServices; +import org.lowcoder.plugin.api.PluginEndpoint; +import org.lowcoder.plugin.api.event.LowcoderEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Component +public class SharedPluginServices implements LowcoderServices +{ + private final PluginEndpointHandler pluginEndpointHandler; + + @Autowired + private ServerConfigRepository serverConfigRepository; + + private List> eventListeners = new LinkedList<>(); + + @Override + public void registerEventListener(Consumer listener) + { + this.eventListeners.add(listener); + } + + @EventListener(classes = LowcoderEvent.class) + private void publishEvents(LowcoderEvent event) + { + for (Consumer listener : eventListeners) + { + listener.accept(event); + } + } + + @Override + public void registerEndpoints(String urlPrefix, List endpoints) + { + pluginEndpointHandler.registerEndpoints(urlPrefix, endpoints); + } + + @Override + public void setConfig(String key, Object value) { + serverConfigRepository.upsert(key, value).block(); + } + + @Override + public Object getConfig(String key) { + return serverConfigRepository.findByKey(key).block(); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java new file mode 100644 index 000000000..aa75bdc17 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java @@ -0,0 +1,198 @@ +package org.lowcoder.api.framework.plugin.data; + +import org.lowcoder.plugin.api.PluginEndpoint; +import org.lowcoder.plugin.api.PluginEndpoint.Method; +import org.lowcoder.plugin.api.data.EndpointRequest; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.server.ServerRequest; + +import java.net.URI; +import java.security.Principal; +import java.util.AbstractMap.SimpleEntry; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; + +public class PluginServerRequest implements EndpointRequest +{ + private URI uri; + private PluginEndpoint.Method method; + private CompletableFuture body; + private Map> headers; + private Map>> cookies; + private Map attributes; + private Map pathVariables; + + private Map> queryParams; + private CompletableFuture principal; + + + public PluginServerRequest() + { + headers = new HashMap<>(); + cookies = new HashMap<>(); + attributes = new HashMap<>(); + pathVariables = new HashMap<>(); + queryParams = new HashMap<>(); + } + + public static PluginServerRequest fromServerRequest(ServerRequest request) + { + PluginServerRequest psr = new PluginServerRequest(); + + psr.uri = request.uri(); + psr.method = fromHttpMetod(request.method()); + psr.body = request.bodyToMono(byte[].class).toFuture(); + + if (request.headers() != null) + { + HttpHeaders httpHeaders = request.headers().asHttpHeaders(); + psr.headers = httpHeaders; + } + + if (request.cookies() != null) + { + request.cookies().entrySet().stream() + .forEach(entry -> { + psr.cookies.put(entry.getKey(), fromHttpCookieList(entry.getValue())); + }); + } + + if (request.attributes() != null) + { + request.attributes().forEach((name, value) -> { + psr.attributes.put(name, value); + }); + } + + if (request.pathVariables() != null) + { + request.pathVariables().entrySet() + .forEach(entry -> { + psr.pathVariables.put(entry.getKey(), entry.getValue()); + }); + } + + if (request.queryParams() != null) + { + request.queryParams().entrySet() + .forEach(entry -> { + psr.queryParams.put(entry.getKey(), entry.getValue()); + }); + } + + psr.principal = request.principal().toFuture(); + + return psr; + } + + private static List> fromHttpCookieList(List cookies) + { + List> list = new LinkedList<>(); + + if (cookies != null) + { + cookies.stream() + .forEach(cookie -> { + list.add(new SimpleEntry(cookie.getName(), cookie.getValue())); + }); + } + + return list; + } + + + + @Override + public URI uri() { + return uri; + } + @Override + public Method method() { + return method; + } + @Override + public CompletableFuture body() { + return body; + } + @Override + public Map> headers() { + return headers; + } + @Override + public Map>> cookies() { + return cookies; + } + @Override + public Map attributes() { + return attributes; + } + @Override + public Map pathVariables() { + return pathVariables; + } + + @Override + public Map> queryParams() { + return queryParams; + } + @Override + public CompletableFuture principal() { + return principal; + } + + + public static HttpMethod fromPluginEndpointMethod(PluginEndpoint.Method method) + { + switch(method) + { + case GET: + return HttpMethod.GET; + case POST: + return HttpMethod.POST; + case PUT: + return HttpMethod.PUT; + case PATCH: + return HttpMethod.PATCH; + case DELETE: + return HttpMethod.DELETE; + case OPTIONS: + return HttpMethod.OPTIONS; + } + return null; + } + + public static PluginEndpoint.Method fromHttpMetod(HttpMethod method) + { + if (method == HttpMethod.GET) + { + return PluginEndpoint.Method.GET; + } + else if (method == HttpMethod.POST) + { + return PluginEndpoint.Method.POST; + } + else if (method == HttpMethod.PUT) + { + return PluginEndpoint.Method.PUT; + } + else if (method == HttpMethod.PATCH) + { + return PluginEndpoint.Method.PATCH; + } + else if (method == HttpMethod.DELETE) + { + return PluginEndpoint.Method.DELETE; + } + else if (method == HttpMethod.OPTIONS) + { + return PluginEndpoint.Method.OPTIONS; + } + return null; + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java new file mode 100644 index 000000000..11922c3dd --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java @@ -0,0 +1,15 @@ +package org.lowcoder.api.framework.plugin.endpoint; + +import java.util.List; + +import org.lowcoder.plugin.api.PluginEndpoint; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +public interface PluginEndpointHandler +{ + public static final String PLUGINS_BASE_URL = "/api/plugins/"; + + void registerEndpoints(String urlPrefix, List endpoints); + List> registeredEndpoints(); +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java new file mode 100644 index 000000000..214252827 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java @@ -0,0 +1,198 @@ +package org.lowcoder.api.framework.plugin.endpoint; + +import static org.springframework.web.reactive.function.server.RequestPredicates.DELETE; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.OPTIONS; +import static org.springframework.web.reactive.function.server.RequestPredicates.PATCH; +import static org.springframework.web.reactive.function.server.RequestPredicates.POST; +import static org.springframework.web.reactive.function.server.RequestPredicates.PUT; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.lowcoder.api.framework.plugin.data.PluginServerRequest; +import org.lowcoder.api.framework.plugin.security.SecuredEndpoint; +import org.lowcoder.plugin.api.EndpointExtension; +import org.lowcoder.plugin.api.PluginEndpoint; +import org.lowcoder.plugin.api.data.EndpointRequest; +import org.lowcoder.plugin.api.data.EndpointResponse; +import org.lowcoder.sdk.exception.BaseException; +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.ProxyFactoryBean; +import org.springframework.aop.target.SimpleBeanTargetSource; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.http.ResponseCookie; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.RequestPredicate; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Slf4j +@RequiredArgsConstructor +@Component +public class PluginEndpointHandlerImpl implements PluginEndpointHandler +{ + private List> routes = new ArrayList<>(); + + private final ApplicationContext applicationContext; + private final DefaultListableBeanFactory beanFactory; + + @Override + public void registerEndpoints(String pluginUrlPrefix, List endpoints) + { + String urlPrefix = PLUGINS_BASE_URL + pluginUrlPrefix; + + if (CollectionUtils.isNotEmpty(endpoints)) + { + for (PluginEndpoint endpoint : endpoints) + { + Method[] handlers = endpoint.getClass().getDeclaredMethods(); + if (handlers != null && handlers.length > 0) + { + for (Method handler : handlers) + { + registerEndpointHandler(urlPrefix, endpoint, handler); + } + } + } + + ((ReloadableRouterFunctionMapping)beanFactory.getBean("routerFunctionMapping")).reloadFunctionMappings(); + } + } + + @Override + public List> registeredEndpoints() + { + return routes; + } + + private void registerEndpointHandler(String urlPrefix, PluginEndpoint endpoint, Method handler) + { + if (!handler.isAnnotationPresent(EndpointExtension.class) || !checkHandlerMethod(handler)) + { + if (handler.isAnnotationPresent(EndpointExtension.class)) + { + log.debug("Not registering plugin endpoint method: {} -> {}! Handler method must be defined as: public EndpointResponse methodName(EndpointRequest request)", endpoint.getClass().getSimpleName(), handler.getName(), handler.getName()); + } + return; + } + + EndpointExtension endpointMeta = handler.getAnnotation(EndpointExtension.class); + String endpointName = endpoint.getClass().getSimpleName() + "_" + handler.getName(); + RouterFunction routerFunction = route(createRequestPredicate(urlPrefix, endpointMeta), req -> runPluginEndpointMethod(endpoint, endpointMeta, handler, req)); + routes.add(routerFunction); + registerRouterFunctionMapping(endpointName, routerFunction); + + log.info("Registered endpoint: {} -> {}: {}", endpoint.getClass().getSimpleName(), endpointMeta.method(), urlPrefix + endpointMeta.uri()); + } + + @SecuredEndpoint + public Mono runPluginEndpointMethod(PluginEndpoint endpoint, EndpointExtension endpointMeta, Method handler, ServerRequest request) + { + Mono result = null; + try + { + log.info("Running plugin endpoint method {}\nRequest: {}", handler.getName(), request); + + EndpointResponse response = (EndpointResponse)handler.invoke(endpoint, PluginServerRequest.fromServerRequest(request)); + result = createServerResponse(response); + } + catch (IllegalAccessException | InvocationTargetException cause) + { + throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !"); + } + return result; + } + + + private void registerRouterFunctionMapping(String endpointName, RouterFunction routerFunction) + { + String beanName = "pluginEndpoint_" + endpointName + "_" + System.currentTimeMillis(); + ((GenericApplicationContext)applicationContext).registerBean(beanName, RouterFunction.class, () -> routerFunction ); + log.debug("Registering RouterFunction bean definition: {}", beanName); + } + + + private Mono createServerResponse(EndpointResponse pluginResponse) + { + /** Create response with given status **/ + BodyBuilder builder = ServerResponse.status(pluginResponse.statusCode()); + + /** Set response headers **/ + if (pluginResponse.headers() != null && !pluginResponse.headers().isEmpty()) + { + pluginResponse.headers().entrySet() + .forEach(entry -> builder.header(entry.getKey(), entry.getValue().toArray(new String[] {}))); + } + + /** Set cookies if available **/ + if (pluginResponse.cookies() != null && !pluginResponse.cookies().isEmpty()) + { + pluginResponse.cookies().values() + .forEach(cookies -> cookies + .forEach(cookie -> builder + .cookie(ResponseCookie.from(cookie.getKey(), cookie.getValue()).build()))); + } + + /** Set response body if available **/ + if (pluginResponse.body() != null) + { + return builder.bodyValue(pluginResponse.body()); + } + + return builder.build(); + } + + private boolean checkHandlerMethod(Method method) + { + ResolvableType returnType = ResolvableType.forMethodReturnType(method); + + return (returnType.getRawClass().isAssignableFrom(EndpointResponse.class) + && method.getParameterCount() == 1 + && method.getParameterTypes()[0].isAssignableFrom(EndpointRequest.class) + ); + } + + private RequestPredicate createRequestPredicate(String basePath, EndpointExtension endpoint) + { + switch(endpoint.method()) + { + case GET: + return GET(pluginEndpointUri(basePath, endpoint.uri())); + case POST: + return POST(pluginEndpointUri(basePath, endpoint.uri())); + case PUT: + return PUT(pluginEndpointUri(basePath, endpoint.uri())); + case PATCH: + return PATCH(pluginEndpointUri(basePath, endpoint.uri())); + case DELETE: + return DELETE(pluginEndpointUri(basePath, endpoint.uri())); + case OPTIONS: + return OPTIONS(pluginEndpointUri(basePath, endpoint.uri())); + } + return null; + } + + private String pluginEndpointUri(String basePath, String uri) + { + return StringUtils.join(basePath, StringUtils.prependIfMissing(uri, "/")); + } + + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java new file mode 100644 index 000000000..42e8e5690 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java @@ -0,0 +1,20 @@ +package org.lowcoder.api.framework.plugin.endpoint; + +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.support.RouterFunctionMapping; + + +public class ReloadableRouterFunctionMapping extends RouterFunctionMapping +{ + /** + * Rescan application context for RouterFunction beans + */ + public void reloadFunctionMappings() + { + initRouterFunctions(); + if (getRouterFunction() != null) + { + RouterFunctions.changeParser(getRouterFunction(), getPathPatternParser()); + } + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java new file mode 100644 index 000000000..6ad509044 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java @@ -0,0 +1,24 @@ +package org.lowcoder.api.framework.plugin.security; + +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class EndpointAuthorizationManager implements AuthorizationManager +{ + + @Override + public AuthorizationDecision check(Supplier authentication, MethodInvocation invocation) + { + log.info("Checking plugin endpoint invocation security for {}", invocation.getMethod().getName()); + + return new AuthorizationDecision(true); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java new file mode 100644 index 000000000..237567643 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java @@ -0,0 +1,92 @@ +package org.lowcoder.api.framework.plugin.security; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.lang3.StringUtils; +import org.lowcoder.plugin.api.EndpointExtension; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.ExpressionAuthorizationDecision; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Slf4j +//@Component +public class PluginAuthorizationManager implements ReactiveAuthorizationManager +{ + private final MethodSecurityExpressionHandler expressionHandler; + + public PluginAuthorizationManager() + { + this.expressionHandler = new DefaultMethodSecurityExpressionHandler(); + } + + @Override + public Mono check(Mono authentication, MethodInvocation invocation) + { + log.info("Checking plugin reactive endpoint invocation security for {}", invocation.getMethod().getName()); + + EndpointExtension endpointExtension = (EndpointExtension)invocation.getArguments()[1]; + if (endpointExtension == null || StringUtils.isBlank(endpointExtension.authorize())) + { + return Mono.empty(); + } + + Expression authorizeExpression = this.expressionHandler.getExpressionParser() + .parseExpression(endpointExtension.authorize()); + + return authentication + .map(auth -> expressionHandler.createEvaluationContext(auth, invocation)) + .flatMap(ctx -> evaluateAsBoolean(authorizeExpression, ctx)) + .map(granted -> new ExpressionAuthorizationDecision(granted, authorizeExpression)); + } + + + private Mono evaluateAsBoolean(Expression expr, EvaluationContext ctx) + { + return Mono.defer(() -> + { + Object value; + try + { + value = expr.getValue(ctx); + } + catch (EvaluationException ex) + { + return Mono.error(() -> new IllegalArgumentException( + "Failed to evaluate expression '" + expr.getExpressionString() + "'", ex)); + } + + if (value instanceof Boolean bool) + { + return Mono.just(bool); + } + + if (value instanceof Mono monoBool) + { + Mono monoValue = monoBool; + return monoValue + .filter(Boolean.class::isInstance) + .map(Boolean.class::cast) + .switchIfEmpty(createInvalidReturnTypeMono(expr)); + } + return createInvalidReturnTypeMono(expr); + }); + } + + private static Mono createInvalidReturnTypeMono(Expression expr) + { + return Mono.error(() -> new IllegalStateException( + "Expression: '" + expr.getExpressionString() + "' must return boolean or Mono")); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java new file mode 100644 index 000000000..aadc0c7fd --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java @@ -0,0 +1,16 @@ +package org.lowcoder.api.framework.plugin.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface SecuredEndpoint { + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java index b933a63e1..e255cc5dc 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java @@ -1,6 +1,24 @@ package org.lowcoder.api.framework.security; +import static org.lowcoder.infra.constant.NewUrl.GITHUB_STAR; +import static org.lowcoder.infra.constant.Url.APPLICATION_URL; +import static org.lowcoder.infra.constant.Url.CONFIG_URL; +import static org.lowcoder.infra.constant.Url.CUSTOM_AUTH; +import static org.lowcoder.infra.constant.Url.DATASOURCE_URL; +import static org.lowcoder.infra.constant.Url.GROUP_URL; +import static org.lowcoder.infra.constant.Url.INVITATION_URL; +import static org.lowcoder.infra.constant.Url.ORGANIZATION_URL; +import static org.lowcoder.infra.constant.Url.QUERY_URL; +import static org.lowcoder.infra.constant.Url.STATE_URL; +import static org.lowcoder.infra.constant.Url.USER_URL; +import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER; +import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER_ID; + +import java.util.List; + +import javax.annotation.Nonnull; + import org.lowcoder.api.authentication.request.AuthRequestFactory; import org.lowcoder.api.authentication.service.AuthenticationApiServiceImpl; import org.lowcoder.api.authentication.util.JWTUtils; @@ -14,7 +32,6 @@ import org.lowcoder.infra.constant.NewUrl; import org.lowcoder.sdk.config.CommonConfig; import org.lowcoder.sdk.util.CookieHelper; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -23,6 +40,7 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity.CsrfSpec; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; @@ -32,48 +50,24 @@ import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.springframework.web.server.adapter.ForwardedHeaderTransformer; -import javax.annotation.Nonnull; -import java.util.List; - -import static org.lowcoder.infra.constant.NewUrl.GITHUB_STAR; -import static org.lowcoder.infra.constant.Url.*; -import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER; -import static org.lowcoder.sdk.constants.Authentication.ANONYMOUS_USER_ID; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor @Configuration @EnableWebFluxSecurity -@EnableReactiveMethodSecurity +@EnableReactiveMethodSecurity(useAuthorizationManager = true) public class SecurityConfig { - @Autowired - private CommonConfig commonConfig; - - @Autowired - private SessionUserService sessionUserService; - - @Autowired - private UserService userService; - - @Autowired - private AccessDeniedHandler accessDeniedHandler; - - @Autowired - private ServerAuthenticationEntryPoint serverAuthenticationEntryPoint; - - @Autowired - private CookieHelper cookieHelper; - - @Autowired - AuthenticationService authenticationService; - - @Autowired - AuthenticationApiServiceImpl authenticationApiService; - - @Autowired - AuthRequestFactory authRequestFactory; - - @Autowired - JWTUtils jwtUtils; + private final CommonConfig commonConfig; + private final SessionUserService sessionUserService; + private final UserService userService; + private final AccessDeniedHandler accessDeniedHandler; + private final ServerAuthenticationEntryPoint serverAuthenticationEntryPoint; + private final CookieHelper cookieHelper; + private final AuthenticationService authenticationService; + private final AuthenticationApiServiceImpl authenticationApiService; + private final AuthRequestFactory authRequestFactory; + private final JWTUtils jwtUtils; @Bean SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { @@ -90,7 +84,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { http .cors(cors -> cors.configurationSource(buildCorsConfigurationSource())) - .csrf(csrf -> csrf.disable()) + .csrf(CsrfSpec::disable) .anonymous(anonymous -> anonymous.principal(createAnonymousUser())) .httpBasic(Customizer.withDefaults()) .authorizeExchange(customizer -> customizer @@ -113,6 +107,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/me"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, USER_URL + "/currentUser"), + ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL + "/lost-password"), + ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL + "/reset-lost-password"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, GROUP_URL + "/list"), // application view ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, QUERY_URL + "/execute"), // application view @@ -139,6 +135,8 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.APPLICATION_URL + "/marketplace-apps"), // marketplace apps ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.USER_URL + "/me"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.USER_URL + "/currentUser"), + ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.USER_URL + "/lost-password"), + ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.USER_URL + "/reset-lost-password"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.GROUP_URL + "/list"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, NewUrl.QUERY_URL + "/execute"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.MATERIAL_URL + "/**"), @@ -146,7 +144,9 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, NewUrl.DATASOURCE_URL + "/jsDatasourcePlugins"), ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/api/docs/**") ) - .permitAll() + .permitAll() + .pathMatchers("/api/plugins/**") + .permitAll() .pathMatchers("/api/**") .authenticated() .pathMatchers("/test/**") @@ -223,7 +223,7 @@ private CorsConfiguration skipCheckCorsForAllowListDomains() { } @Bean - public ForwardedHeaderTransformer forwardedHeaderTransformer() { + ForwardedHeaderTransformer forwardedHeaderTransformer() { return new ForwardedHeaderTransformer(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java index 69e4517d5..fcb066195 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java @@ -182,6 +182,11 @@ private Mono removePermissions(String folderId) { public Mono update(Folder folder) { Folder newFolder = new Folder(); newFolder.setName(folder.getName()); + newFolder.setTitle(folder.getTitle()); + newFolder.setType(folder.getType()); + newFolder.setCategory(folder.getCategory()); + newFolder.setDescription(folder.getDescription()); + newFolder.setImage(folder.getImage()); return checkManagePermission(folder.getId()) .then(folderService.updateById(folder.getId(), newFolder)) .then(folderService.findById(folder.getId())) @@ -241,7 +246,7 @@ public Flux getElements(@Nullable String folderId, @Nullable ApplicationType if (folderInfoView == null) { return; } - folderInfoView.setManageable(orgMember.isAdmin() || orgMember.getUserId().equals(folderInfoView.getCreateBy())); + folderInfoView.setManageable(orgMember.isAdmin() || orgMember.isSuperAdmin() || orgMember.getUserId().equals(folderInfoView.getCreateBy())); List folderInfoViews = folderNode.getFolderChildren().stream().filter(FolderInfoView::isVisible).toList(); folderInfoView.setSubFolders(folderInfoViews); @@ -335,7 +340,7 @@ private Mono> buildApplicationInfoView private Mono checkManagePermission(String folderId) { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.just(orgMember); } return isCreator(folderId) @@ -421,6 +426,10 @@ public Mono buildFolderInfoView(Folder folder, boolean visible, .folderId(folder.getId()) .parentFolderId(folder.getParentFolderId()) .name(folder.getName()) + .description(folder.getDescription()) + .category(folder.getCategory()) + .type(folder.getType()) + .image(folder.getImage()) .createAt(folder.getCreatedAt() == null ? 0 : folder.getCreatedAt().toEpochMilli()) .createBy(user.getName()) .createTime(folder.getCreatedAt()) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java index ae7e2f2c0..4f07b0342 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java @@ -1,6 +1,6 @@ package org.lowcoder.api.home; -import static org.lowcoder.infra.event.EventType.APPLICATION_MOVE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_MOVE; import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; @@ -13,7 +13,11 @@ import org.lowcoder.domain.folder.model.Folder; import org.lowcoder.domain.folder.service.FolderService; import org.lowcoder.domain.permission.model.ResourceRole; -import org.lowcoder.infra.event.EventType; +import org.lowcoder.infra.constant.NewUrl; +import org.lowcoder.plugin.api.event.LowcoderEvent.EventType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java index b1abb505f..17776f298 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java @@ -20,6 +20,11 @@ public class FolderInfoView { private final String folderId; private final String parentFolderId; private final String name; + private final String title; + private final String description; + private final String category; + private final String type; + private final String image; private final Long createAt; private final String createBy; private boolean isVisible; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java index 9104839d9..a96485eae 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java @@ -18,6 +18,8 @@ public interface SessionUserService { @NonEmptyMono Mono getVisitorOrgMemberCache(); + Mono getVisitorOrgMemberCacheSilent(); + Mono getVisitorOrgMember(); Mono isAnonymousUser(); @@ -33,4 +35,6 @@ public interface SessionUserService { Mono resolveSessionUserForJWT(Claims claims, String token); Mono tokenExist(String token); + + Mono getVisitorToken(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java index 5c0b5e1fe..75b5bec8d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java @@ -1,6 +1,7 @@ package org.lowcoder.api.home; import static org.lowcoder.sdk.constants.GlobalContext.CURRENT_ORG_MEMBER; +import static org.lowcoder.sdk.constants.GlobalContext.VISITOR_TOKEN; import static org.lowcoder.sdk.exception.BizError.UNABLE_TO_FIND_VALID_ORG; import static org.lowcoder.sdk.util.ExceptionUtils.deferredError; import static org.lowcoder.sdk.util.JsonUtils.fromJsonQuietly; @@ -74,6 +75,17 @@ public Mono getVisitorOrgMemberCache() { .switchIfEmpty(deferredError(UNABLE_TO_FIND_VALID_ORG, "UNABLE_TO_FIND_VALID_ORG")); } + @Override + public Mono getVisitorOrgMemberCacheSilent() { + return Mono.deferContextual(contextView -> (Mono) contextView.get(CURRENT_ORG_MEMBER)) + .delayUntil(Mono::just); + } + + @Override + public Mono getVisitorToken() { + return Mono.deferContextual(contextView -> Mono.just(contextView.get(VISITOR_TOKEN))); + } + @Override public Mono getVisitorOrgMember() { return getVisitorId() diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/permission/PermissionHelper.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/permission/PermissionHelper.java index e7373aae9..48a601edb 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/permission/PermissionHelper.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/permission/PermissionHelper.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.Locale; -import javax.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotEmpty; import org.lowcoder.api.permission.view.PermissionItemView; import org.lowcoder.domain.group.model.Group; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java index 968fabc2c..99702c6f2 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java @@ -11,7 +11,7 @@ import org.lowcoder.api.util.BusinessEventPublisher; import org.lowcoder.domain.query.model.LibraryQuery; import org.lowcoder.domain.query.service.LibraryQueryService; -import org.lowcoder.infra.event.EventType; +import org.lowcoder.plugin.api.event.LowcoderEvent.EventType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java index c25c78cd4..0bd0300da 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java @@ -58,6 +58,9 @@ public Mono getGroupMembers(String groupId, int page, Mono visitorRoleMono = groupAndOrgMemberInfo.flatMap(tuple -> { GroupMember groupMember = tuple.getT1(); OrgMember orgMember = tuple.getT2(); + if (groupMember.isSuperAdmin() || orgMember.isSuperAdmin()) { + return Mono.just(MemberRole.SUPER_ADMIN); + } if (groupMember.isAdmin() || orgMember.isAdmin()) { return Mono.just(MemberRole.ADMIN); } @@ -109,7 +112,7 @@ private boolean hasReadPermission(Tuple2 tuple) { private boolean hasManagePermission(Tuple2 tuple) { GroupMember groupMember = tuple.getT1(); OrgMember orgMember = tuple.getT2(); - return groupMember.isAdmin() || orgMember.isAdmin(); + return groupMember.isAdmin() || orgMember.isAdmin() || groupMember.isSuperAdmin() || orgMember.isSuperAdmin(); } private Mono> getGroupAndOrgMemberInfo(String groupId) { @@ -175,10 +178,16 @@ public Mono> getGroups() { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { String orgId = orgMember.getOrgId(); - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { + MemberRole memberRole; + if(orgMember.isAdmin()) { + memberRole = MemberRole.ADMIN; + } else { + memberRole = MemberRole.SUPER_ADMIN; + } return groupService.getByOrgId(orgId) .sort() - .flatMapSequential(group -> GroupView.from(group, MemberRole.ADMIN.getValue())) + .flatMapSequential(group -> GroupView.from(group, memberRole.getValue())) .collectList(); } return groupMemberService.getUserGroupMembersInOrg(orgId, orgMember.getUserId()) @@ -211,7 +220,7 @@ public Mono deleteGroup(String groupId) { public Mono create(CreateGroupRequest createGroupRequest) { return sessionUserService.getVisitorOrgMemberCache() - .filter(OrgMember::isAdmin) + .filter(orgMember -> orgMember.isAdmin() || orgMember.isSuperAdmin()) .switchIfEmpty(deferredError(BizError.NOT_AUTHORIZED, NOT_AUTHORIZED)) .delayUntil(orgMember -> bizThresholdChecker.checkMaxGroupCount(orgMember)) .flatMap(orgMember -> { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java index ebe9d26b8..b28ffef30 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java @@ -4,7 +4,7 @@ import java.util.List; -import javax.validation.Valid; +import jakarta.validation.Valid; import org.apache.commons.lang3.StringUtils; import org.lowcoder.api.framework.view.ResponseView; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java index 3f4d60242..c1d5fd5e7 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java @@ -2,7 +2,7 @@ import java.util.List; -import javax.validation.Valid; +import jakarta.validation.Valid; import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.api.usermanagement.view.AddMemberRequest; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java index 6663e09cb..ac3023f74 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java @@ -270,7 +270,7 @@ public Mono create(Organization organization) { return sessionUserService.getVisitorId() .delayUntil(userId -> bizThresholdChecker.checkMaxOrgCount(userId)) .delayUntil(__ -> checkIfSaasMode()) - .flatMap(userId -> organizationService.create(organization, userId)) + .flatMap(userId -> organizationService.create(organization, userId, false)) .map(OrgView::new); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java index fc247766a..315c5f6f2 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgDevChecker.java @@ -44,7 +44,7 @@ public Mono checkCurrentOrgDev() { public Mono isCurrentOrgDev() { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.just(true); } return inDevGroup(orgMember.getOrgId(), orgMember.getUserId()); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java index 484b1aead..ab8a0e78f 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java @@ -2,7 +2,7 @@ import java.util.List; -import javax.validation.Valid; +import jakarta.validation.Valid; import org.lowcoder.api.authentication.dto.OrganizationDomainCheckResult; import org.lowcoder.api.framework.view.ResponseView; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java index a5ba2774c..5e57bcd81 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java @@ -2,7 +2,7 @@ import java.util.List; -import javax.validation.Valid; +import jakarta.validation.Valid; import org.lowcoder.api.framework.view.ResponseView; import org.lowcoder.api.usermanagement.view.OrgMemberListView; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java index 42161bd5a..2b5ce21fb 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java @@ -46,7 +46,7 @@ public Mono getUserDetailById(String userId) { private Mono checkAdminPermissionAndUserBelongsToCurrentOrg(String userId) { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (!orgMember.isAdmin()) { + if (!orgMember.isAdmin() && !orgMember.isSuperAdmin()) { return ofError(UNSUPPORTED_OPERATION, "BAD_REQUEST"); } return orgMemberService.getOrgMember(orgMember.getOrgId(), userId) @@ -65,6 +65,14 @@ public Mono resetPassword(String userId) { .then(userService.resetPassword(userId)); } + public Mono lostPassword(String userEmail) { + return userService.lostPassword(userEmail); + } + + public Mono resetLostPassword(String userEmail, String token, String newPassword) { + return userService.resetLostPassword(userEmail, token, newPassword); + } + // ========================== TOKEN OPERATIONS START ========================== public Mono saveToken(String userId, String source, String token) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java index d865cba4f..56cccffd5 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserController.java @@ -146,6 +146,26 @@ public Mono> resetPassword(@RequestBody ResetPasswordReques } + @Override + public Mono> lostPassword(@RequestBody LostPasswordRequest request) { + if (StringUtils.isBlank(request.userEmail())) { + return Mono.empty(); + } + return userApiService.lostPassword(request.userEmail()) + .map(ResponseView::success); + } + + @Override + public Mono> resetLostPassword(@RequestBody ResetLostPasswordRequest request) { + if (StringUtils.isBlank(request.userEmail()) || StringUtils.isBlank(request.token()) + || StringUtils.isBlank(request.newPassword())) { + return ofError(BizError.INVALID_PARAMETER, "INVALID_PARAMETER"); + } + + return userApiService.resetLostPassword(request.userEmail(), request.token(), request.newPassword()) + .map(ResponseView::success); + } + @Override public Mono> setPassword(@RequestParam String password) { if (StringUtils.isBlank(password)) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java index 96c93c472..196925134 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserEndpoints.java @@ -121,6 +121,12 @@ public interface UserEndpoints @PostMapping("/reset-password") public Mono> resetPassword(@RequestBody ResetPasswordRequest request); + @PostMapping("/lost-password") + public Mono> lostPassword(@RequestBody LostPasswordRequest request); + + @PostMapping("/reset-lost-password") + public Mono> resetLostPassword(@RequestBody ResetLostPasswordRequest request); + @Operation( tags = TAG_USER_PASSWORD_MANAGEMENT, operationId = "setPassword", @@ -151,6 +157,12 @@ public interface UserEndpoints public record ResetPasswordRequest(String userId) { } + public record LostPasswordRequest(String userEmail) { + } + + public record ResetLostPasswordRequest(String token, String userEmail, String newPassword) { + } + public record UpdatePasswordRequest(String oldPassword, String newPassword) { } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/CreateGroupRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/CreateGroupRequest.java index b831285b5..32237cc73 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/CreateGroupRequest.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/view/CreateGroupRequest.java @@ -1,6 +1,6 @@ package org.lowcoder.api.usermanagement.view; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; public class CreateGroupRequest { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/ApiCallEventPublisher.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/ApiCallEventPublisher.java new file mode 100644 index 000000000..109d5abd5 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/ApiCallEventPublisher.java @@ -0,0 +1,90 @@ +package org.lowcoder.api.util; + +import com.google.common.hash.Hashing; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.lowcoder.api.framework.filter.ReactiveRequestContextHolder; +import org.lowcoder.api.home.SessionUserService; +import org.lowcoder.domain.organization.model.OrgMember; +import org.lowcoder.infra.event.APICallEvent; +import org.lowcoder.plugin.api.event.LowcoderEvent.EventType; +import org.lowcoder.sdk.constants.Authentication; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +import static org.springframework.http.HttpHeaders.writableHttpHeaders; + +@Slf4j +@Aspect +@Component +public class ApiCallEventPublisher { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + @Autowired + private SessionUserService sessionUserService; + + @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)") + public void getMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") + public void postMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)") + public void putMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.DeleteMapping)") + public void deleteMapping(){} + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PatchMapping)") + public void patchMapping(){} + + @Around("(getMapping() || postMapping() || putMapping() || deleteMapping() || patchMapping())") + public Object handleAPICallEvent(ProceedingJoinPoint joinPoint) throws Throwable { + + return sessionUserService.getVisitorToken() + .zipWith(sessionUserService.getVisitorOrgMemberCacheSilent().defaultIfEmpty(OrgMember.NOT_EXIST)) + .zipWith(ReactiveRequestContextHolder.getRequest()) + .doOnNext( + tuple -> { + String token = tuple.getT1().getT1(); + OrgMember orgMember = tuple.getT1().getT2(); + ServerHttpRequest request = tuple.getT2(); + if (orgMember == OrgMember.NOT_EXIST) { + return; + } + MultiValueMap headers = writableHttpHeaders(request.getHeaders()); + headers.remove("Cookie"); + String ipAddress = headers.remove("X-Real-IP").stream().findFirst().get(); + APICallEvent event = APICallEvent.builder() + .userId(orgMember.getUserId()) + .orgId(orgMember.getOrgId()) + .type(EventType.API_CALL_EVENT) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) + .httpMethod(request.getMethod().name()) + .requestUri(request.getURI().getPath()) + .headers(headers) + .queryParams(request.getQueryParams()) + .ipAddress(ipAddress) + .build(); + event.populateDetails(); + applicationEventPublisher.publishEvent(event); + }) + .onErrorResume(throwable -> { + log.error("handleAPICallEvent error {} for: {} ", joinPoint.getSignature().getName(), EventType.API_CALL_EVENT, throwable); + return Mono.empty(); + }) + .then((Mono) joinPoint.proceed()); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java index e81f5136c..850c33d78 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/BusinessEventPublisher.java @@ -1,15 +1,7 @@ package org.lowcoder.api.util; -import static org.lowcoder.domain.permission.model.ResourceHolder.USER; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Optional; - -import javax.annotation.Nullable; - +import com.google.common.hash.Hashing; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.lowcoder.api.application.view.ApplicationInfoView; import org.lowcoder.api.application.view.ApplicationView; @@ -32,7 +24,6 @@ import org.lowcoder.domain.user.model.User; import org.lowcoder.domain.user.service.UserService; import org.lowcoder.infra.event.ApplicationCommonEvent; -import org.lowcoder.infra.event.EventType; import org.lowcoder.infra.event.FolderCommonEvent; import org.lowcoder.infra.event.LibraryQueryEvent; import org.lowcoder.infra.event.QueryExecutionEvent; @@ -47,14 +38,20 @@ import org.lowcoder.infra.event.groupmember.GroupMemberRoleUpdateEvent; import org.lowcoder.infra.event.user.UserLoginEvent; import org.lowcoder.infra.event.user.UserLogoutEvent; +import org.lowcoder.plugin.api.event.LowcoderEvent.EventType; +import org.lowcoder.sdk.constants.Authentication; import org.lowcoder.sdk.util.LocaleUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; - -import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; +import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static org.lowcoder.domain.permission.model.ResourceHolder.USER; + @Slf4j @Component public class BusinessEventPublisher { @@ -77,16 +74,24 @@ public class BusinessEventPublisher { private ResourcePermissionService resourcePermissionService; public Mono publishFolderCommonEvent(String folderId, String folderName, EventType eventType) { - return sessionUserService.getVisitorOrgMemberCache() - .doOnNext(orgMember -> { - FolderCommonEvent event = FolderCommonEvent.builder() - .id(folderId) - .name(folderName) - .userId(orgMember.getUserId()) - .orgId(orgMember.getOrgId()) - .type(eventType) - .build(); - applicationEventPublisher.publishEvent(event); + + return sessionUserService.getVisitorToken() + .zipWith(sessionUserService.getVisitorOrgMemberCache()) + .doOnNext( + tuple -> { + String token = tuple.getT1(); + OrgMember orgMember = tuple.getT2(); + FolderCommonEvent event = FolderCommonEvent.builder() + .id(folderId) + .name(folderName) + .userId(orgMember.getUserId()) + .orgId(orgMember.getOrgId()) + .type(eventType) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) + .build(); + event.populateDetails(); + applicationEventPublisher.publishEvent(event); }) .then() .onErrorResume(throwable -> { @@ -106,6 +111,7 @@ public Mono publishApplicationCommonEvent(String applicationId, @Nullable return ApplicationView.builder() .applicationInfoView(applicationInfoView) .build(); + }) .flatMap(applicationView -> publishApplicationCommonEvent(applicationView, eventType)); } @@ -126,9 +132,11 @@ public Mono publishApplicationCommonEvent(ApplicationView applicationView, .map(Optional::of) .onErrorReturn(Optional.empty()); })) + .zipWith(sessionUserService.getVisitorToken()) .doOnNext(tuple -> { - OrgMember orgMember = tuple.getT1(); - Optional optional = tuple.getT2(); + OrgMember orgMember = tuple.getT1().getT1(); + Optional optional = tuple.getT1().getT2(); + String token = tuple.getT2(); ApplicationInfoView applicationInfoView = applicationView.getApplicationInfoView(); ApplicationCommonEvent event = ApplicationCommonEvent.builder() .orgId(orgMember.getOrgId()) @@ -138,7 +146,10 @@ public Mono publishApplicationCommonEvent(ApplicationView applicationView, .type(eventType) .folderId(optional.map(Folder::getId).orElse(null)) .folderName(optional.map(Folder::getName).orElse(null)) + .isAnonymous(anonymous) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); }) .then() @@ -150,13 +161,18 @@ public Mono publishApplicationCommonEvent(ApplicationView applicationView, } public Mono publishUserLoginEvent(String source) { - return sessionUserService.getVisitorOrgMember() - .doOnNext(orgMember -> { + return sessionUserService.getVisitorOrgMember().zipWith(sessionUserService.getVisitorToken()) + .doOnNext(tuple -> { + OrgMember orgMember = tuple.getT1(); + String token = tuple.getT2(); UserLoginEvent event = UserLoginEvent.builder() .orgId(orgMember.getOrgId()) .userId(orgMember.getUserId()) .source(source) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); }) .then() @@ -168,11 +184,17 @@ public Mono publishUserLoginEvent(String source) { public Mono publishUserLogoutEvent() { return sessionUserService.getVisitorOrgMemberCache() - .doOnNext(orgMember -> { + .zipWith(sessionUserService.getVisitorToken()) + .doOnNext(tuple -> { + OrgMember orgMember = tuple.getT1(); + String token = tuple.getT2(); UserLogoutEvent event = UserLogoutEvent.builder() .orgId(orgMember.getOrgId()) .userId(orgMember.getUserId()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); }) .then() @@ -184,15 +206,19 @@ public Mono publishUserLogoutEvent() { public Mono publishGroupCreateEvent(Group group) { return sessionUserService.getVisitorOrgMemberCache() - .delayUntil(orgMember -> + .zipWith(sessionUserService.getVisitorToken()) + .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); GroupCreateEvent event = GroupCreateEvent.builder() - .orgId(orgMember.getOrgId()) - .userId(orgMember.getUserId()) + .orgId(tuple.getT1().getOrgId()) + .userId(tuple.getT1().getUserId()) .groupId(group.getId()) .groupName(group.getName(locale)) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -208,15 +234,19 @@ public Mono publishGroupUpdateEvent(boolean publish, Group previousGroup, return Mono.empty(); } return sessionUserService.getVisitorOrgMemberCache() - .delayUntil(orgMember -> + .zipWith(sessionUserService.getVisitorToken()) + .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); GroupUpdateEvent event = GroupUpdateEvent.builder() - .orgId(orgMember.getOrgId()) - .userId(orgMember.getUserId()) + .orgId(tuple.getT1().getOrgId()) + .userId(tuple.getT1().getUserId()) .groupId(previousGroup.getId()) .groupName(previousGroup.getName(locale) + " => " + newGroupName) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -232,15 +262,19 @@ public Mono publishGroupDeleteEvent(boolean publish, Group previousGroup) return Mono.empty(); } return sessionUserService.getVisitorOrgMemberCache() - .delayUntil(orgMember -> + .zipWith(sessionUserService.getVisitorToken()) + .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); GroupDeleteEvent event = GroupDeleteEvent.builder() - .orgId(orgMember.getOrgId()) - .userId(orgMember.getUserId()) + .orgId(tuple.getT1().getOrgId()) + .userId(tuple.getT1().getUserId()) .groupId(previousGroup.getId()) .groupName(previousGroup.getName(locale)) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -257,13 +291,15 @@ public Mono publishGroupMemberAddEvent(boolean publish, String groupId, Ad } return Mono.zip(groupService.getById(groupId), sessionUserService.getVisitorOrgMemberCache(), - userService.findById(addMemberRequest.getUserId())) + userService.findById(addMemberRequest.getUserId()), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); Group group = tuple.getT1(); OrgMember orgMember = tuple.getT2(); User member = tuple.getT3(); + String token = tuple.getT4(); GroupMemberAddEvent event = GroupMemberAddEvent.builder() .orgId(orgMember.getOrgId()) .userId(orgMember.getUserId()) @@ -272,7 +308,10 @@ public Mono publishGroupMemberAddEvent(boolean publish, String groupId, Ad .memberId(member.getId()) .memberName(member.getName()) .memberRole(addMemberRequest.getRole()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(token, StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -290,7 +329,8 @@ public Mono publishGroupMemberRoleUpdateEvent(boolean publish, String grou } return Mono.zip(groupService.getById(groupId), sessionUserService.getVisitorOrgMemberCache(), - userService.findById(previousGroupMember.getUserId())) + userService.findById(previousGroupMember.getUserId()), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); @@ -305,7 +345,10 @@ public Mono publishGroupMemberRoleUpdateEvent(boolean publish, String grou .memberId(member.getId()) .memberName(member.getName()) .memberRole(previousGroupMember.getRole().getValue() + " => " + updateRoleRequest.getRole()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT4(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -322,7 +365,8 @@ public Mono publishGroupMemberLeaveEvent(boolean publish, GroupMember grou } return Mono.zip(groupService.getById(groupMember.getGroupId()), userService.findById(groupMember.getUserId()), - sessionUserService.getVisitorOrgMemberCache()) + sessionUserService.getVisitorOrgMemberCache(), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); @@ -337,7 +381,10 @@ public Mono publishGroupMemberLeaveEvent(boolean publish, GroupMember grou .memberId(user.getId()) .memberName(user.getName()) .memberRole(groupMember.getRole().getValue()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT4(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -354,7 +401,8 @@ public Mono publishGroupMemberRemoveEvent(boolean publish, GroupMember pre } return Mono.zip(sessionUserService.getVisitorOrgMemberCache(), groupService.getById(previousGroupMember.getGroupId()), - userService.findById(previousGroupMember.getUserId())) + userService.findById(previousGroupMember.getUserId()), + sessionUserService.getVisitorToken()) .delayUntil(tuple -> Mono.deferContextual(contextView -> { Locale locale = LocaleUtils.getLocale(contextView); @@ -369,7 +417,10 @@ public Mono publishGroupMemberRemoveEvent(boolean publish, GroupMember pre .memberId(member.getId()) .memberName(member.getName()) .memberRole(previousGroupMember.getRole().getValue()) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT4(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono.empty(); })) @@ -395,15 +446,19 @@ public Mono publishDatasourceEvent(String id, EventType eventType) { public Mono publishDatasourceEvent(Datasource datasource, EventType eventType) { return sessionUserService.getVisitorOrgMemberCache() - .flatMap(orgMember -> { + .zipWith(sessionUserService.getVisitorToken()) + .flatMap(tuple -> { DatasourceEvent event = DatasourceEvent.builder() .datasourceId(datasource.getId()) .name(datasource.getName()) .type(datasource.getType()) .eventType(eventType) - .userId(orgMember.getUserId()) - .orgId(orgMember.getOrgId()) + .userId(tuple.getT1().getUserId()) + .orgId(tuple.getT1().getOrgId()) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) .build(); + event.populateDetails(); applicationEventPublisher.publishEvent(event); return Mono. empty(); }) @@ -435,7 +490,9 @@ public Mono publishDatasourcePermissionEvent(String permissionId, EventTyp public Mono publishDatasourcePermissionEvent(String datasourceId, Collection userIds, Collection groupIds, String role, EventType eventType) { - return Mono.zip(sessionUserService.getVisitorOrgMemberCache(), datasourceService.getById(datasourceId)) + return Mono.zip(sessionUserService.getVisitorOrgMemberCache(), + datasourceService.getById(datasourceId), + sessionUserService.getVisitorToken()) .flatMap(tuple -> { OrgMember orgMember = tuple.getT1(); Datasource datasource = tuple.getT2(); @@ -449,7 +506,10 @@ public Mono publishDatasourcePermissionEvent(String datasourceId, .groupIds(groupIds) .role(role) .eventType(eventType) + .isAnonymous(Authentication.isAnonymousUser(orgMember.getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT3(), StandardCharsets.UTF_8).toString()) .build(); + datasourcePermissionEvent.populateDetails(); applicationEventPublisher.publishEvent(datasourcePermissionEvent); return Mono. empty(); }) @@ -465,13 +525,20 @@ public Mono publishLibraryQuery(LibraryQuery libraryQuery, EventType event public Mono publishLibraryQueryEvent(String id, String name, EventType eventType) { return sessionUserService.getVisitorOrgMemberCache() - .map(orgMember -> LibraryQueryEvent.builder() - .userId(orgMember.getUserId()) - .orgId(orgMember.getOrgId()) - .id(id) - .name(name) - .eventType(eventType) - .build()) + .zipWith(sessionUserService.getVisitorToken()) + .map(tuple -> { + LibraryQueryEvent event = LibraryQueryEvent.builder() + .userId(tuple.getT1().getUserId()) + .orgId(tuple.getT1().getOrgId()) + .id(id) + .name(name) + .eventType(eventType) + .isAnonymous(Authentication.isAnonymousUser(tuple.getT1().getUserId())) + .sessionHash(Hashing.sha512().hashString(tuple.getT2(), StandardCharsets.UTF_8).toString()) + .build(); + event.populateDetails(); + return event; + }) .doOnNext(applicationEventPublisher::publishEvent) .then() .onErrorResume(throwable -> { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/RandomPasswordGeneratorConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/RandomPasswordGeneratorConfig.java new file mode 100644 index 000000000..57701daa8 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/RandomPasswordGeneratorConfig.java @@ -0,0 +1,28 @@ +package org.lowcoder.api.util; + +import org.passay.CharacterData; +import org.passay.CharacterRule; +import org.passay.EnglishCharacterData; +import org.passay.PasswordGenerator; + +public class RandomPasswordGeneratorConfig { + + public String generatePassayPassword() { + PasswordGenerator gen = new PasswordGenerator(); + CharacterData lowerCaseChars = EnglishCharacterData.LowerCase; + CharacterRule lowerCaseRule = new CharacterRule(lowerCaseChars); + lowerCaseRule.setNumberOfCharacters(3); + + CharacterData upperCaseChars = EnglishCharacterData.UpperCase; + CharacterRule upperCaseRule = new CharacterRule(upperCaseChars); + upperCaseRule.setNumberOfCharacters(3); + + CharacterData digitChars = EnglishCharacterData.Digit; + CharacterRule digitRule = new CharacterRule(digitChars); + digitRule.setNumberOfCharacters(3); + + + String password = gen.generatePassword(10, lowerCaseRule, upperCaseRule, digitRule); + return password; + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java index 6e33d075b..5364a5931 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java @@ -18,6 +18,7 @@ import org.lowcoder.infra.config.model.ServerConfig; import org.lowcoder.infra.eventlog.EventLog; import org.lowcoder.infra.serverlog.ServerLog; +import org.lowcoder.runner.migrations.job.AddSuperAdminUser; import org.lowcoder.runner.migrations.job.AddPtmFieldsJob; import org.lowcoder.runner.migrations.job.CompleteAuthType; import org.lowcoder.runner.migrations.job.MigrateAuthConfigJob; @@ -183,7 +184,12 @@ public void addOrgIdIndexOnServerLog(MongockTemplate mongoTemplate) { ); } - @ChangeSet(order = "020", id = "add-ptm-fields-to-applications", author = "") + @ChangeSet(order = "020", id = "add-super-admin-user", author = "") + public void addSuperAdminUser(AddSuperAdminUser addSuperAdminUser) { + addSuperAdminUser.addSuperAdmin(); + } + + @ChangeSet(order = "021", id = "add-ptm-fields-to-applications", author = "") public void addPtmFieldsToApplicatgions(AddPtmFieldsJob addPtmFieldsJob) { addPtmFieldsJob.migrateApplicationsToInitPtmFields(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUser.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUser.java new file mode 100644 index 000000000..2aea53af3 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUser.java @@ -0,0 +1,6 @@ +package org.lowcoder.runner.migrations.job; + +public interface AddSuperAdminUser { + + void addSuperAdmin(); +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUserImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUserImpl.java new file mode 100644 index 000000000..72e7391d7 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUserImpl.java @@ -0,0 +1,67 @@ +package org.lowcoder.runner.migrations.job; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.lowcoder.api.authentication.service.AuthenticationApiServiceImpl; +import org.lowcoder.api.util.RandomPasswordGeneratorConfig; +import org.lowcoder.domain.authentication.context.AuthRequestContext; +import org.lowcoder.domain.authentication.context.FormAuthRequestContext; +import org.lowcoder.domain.user.model.AuthUser; +import org.lowcoder.sdk.config.CommonConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import static org.lowcoder.domain.authentication.AuthenticationService.DEFAULT_AUTH_CONFIG; + +@RequiredArgsConstructor +@Component +@Slf4j(topic = "AddSuperAdminUserImpl") +public class AddSuperAdminUserImpl implements AddSuperAdminUser { + + private final AuthenticationApiServiceImpl authenticationApiService; + private final CommonConfig commonConfig; + + @Override + public void addSuperAdmin() { + + AuthUser authUser = formulateAuthUser(); + + authenticationApiService.updateOrCreateUser(authUser, false) + .delayUntil(user -> { + if (user.getIsNewUser()) { + return authenticationApiService.onUserRegister(user, true); + } + return Mono.empty(); + }) + .block(); + } + + private AuthUser formulateAuthUser() { + String username = formulateUserName(); + String password = formulatePassword(); + AuthRequestContext authRequestContext = new FormAuthRequestContext(username, password, true, null); + authRequestContext.setAuthConfig(DEFAULT_AUTH_CONFIG); + return AuthUser.builder() + .uid(username) + .username(username) + .authContext(authRequestContext) + .build(); + } + private String formulateUserName() { + if(commonConfig.getSuperAdmin().getUserName() != null) { + return commonConfig.getSuperAdmin().getUserName(); + } + return "admin@lowcoder.pro"; + } + + private String formulatePassword() { + if(commonConfig.getSuperAdmin().getPassword() != null) { + return commonConfig.getSuperAdmin().getPassword(); + } + RandomPasswordGeneratorConfig passGen = new RandomPasswordGeneratorConfig(); + String password = passGen.generatePassayPassword(); + log.info("PASSWORD FOR SUPER-ADMIN is: {}", password); + return password; + } +} diff --git a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml index 66d022e68..ee0ecf3e3 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml +++ b/server/api-service/lowcoder-server/src/main/resources/application-lowcoder.yml @@ -9,8 +9,36 @@ spring: main: allow-bean-definition-overriding: true allow-circular-references: true + mail: + host: smtp.gmail.com + port: 587 + username: yourmail@gmail.com + password: yourpass + properties: + mail: + smtp: + auth: true + ssl: + enable: false + starttls: + enable: true + required: true + transport: + protocol: smtp + +logging: + level: + root: info + web: debug + +logging: + level: + root: info + web: debug server: + error: + includeStacktrace: ALWAYS compression: enabled: true forward-headers-strategy: NATIVE @@ -44,8 +72,15 @@ common: block-hound-enable: false js-executor: host: http://127.0.0.1:6060 + plugin-dirs: + - /tmp/plugins + super-admin: + username: test@lowcoder.pro + password: Password@123 marketplace: private-mode: false + lowcoder-public-url: http://localhost:8080 + notifications-email-sender: info@lowcoder.org material: mongodb-grid-fs: diff --git a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml index 258833aea..8e51ca239 100644 --- a/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml +++ b/server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml @@ -17,8 +17,23 @@ spring: codec: max-in-memory-size: 20MB webflux: - context-path: / - + base-path: / + mail: + host: ${LOWCODER_ADMIN_SMTP_HOST:smtp.gmail.com} + port: ${LOWCODER_ADMIN_SMTP_PORT:587} + username: ${LOWCODER_ADMIN_SMTP_USERNAME:yourmail@gmail.com} + password: ${LOWCODER_ADMIN_SMTP_PASSWORD:yourpass} + properties: + mail: + smtp: + auth: ${LOWCODER_ADMIN_SMTP_AUTH:true} + ssl: + enable: ${LOWCODER_ADMIN_SMTP_SSL_ENABLED:false} + starttls: + enable: ${LOWCODER_ADMIN_SMTP_STARTTLS_ENABLED:true} + required: ${LOWCODER_ADMIN_SMTP_STARTTLS_REQUIRED:true} + transport: + protocol: smtp server: compression: enabled: true @@ -53,8 +68,12 @@ common: max-query-timeout: ${LOWCODER_MAX_QUERY_TIMEOUT:120} workspace: mode: ${LOWCODER_WORKSPACE_MODE:SAAS} + plugin-dirs: + - ${LOWCODER_PLUGINS_DIR:plugins} marketplace: private-mode: ${LOWCODER_MARKETPLACE_PRIVATE_MODE:true} + lowcoder-public-url: ${LOWCODER_PUBLIC_URL:http://localhost:8080} + notifications-email-sender: ${LOWCODER_LOST_PASSWORD_EMAIL_SENDER:info@lowcoder.org} material: mongodb-grid-fs: diff --git a/server/api-service/pom.xml b/server/api-service/pom.xml index 23ffce7ad..8ec6f774d 100644 --- a/server/api-service/pom.xml +++ b/server/api-service/pom.xml @@ -1,335 +1,151 @@ - - - - org.springframework.boot - spring-boot-starter-parent - 3.1.1 - - - - 4.0.0 - org.lowcoder - lowcoder-root - ${revision} - pom - lowcoder-root - - - 2.3.0-SNAPSHOT - 17 - true - true - true - org.lowcoder - 1.0-SNAPSHOT - true - 2.17.0 - 17 - 17 - - - - - sonatype - https://oss.sonatype.org/content/repositories/snapshots - - - - - - - cloud - - cloud - - - true - - - - - false - src/main/java - - **/*.java - - - - src/main/resources - - **/selfhost/application*.yml - - - - - - - selfhost - - selfhost - - - - - false - src/main/java - - **/*.java - - - - src/main/resources - - **/application*.yml - - - - - - - - - - - org.codehaus.mojo - license-maven-plugin - 2.0.0 - - - maven-dependency-plugin - 3.1.2 - - - - - - - + + + 4.0.0 + org.lowcoder + lowcoder-root + pom + lowcoder-root + ${revision} + + + + 2.4.0 + 17 + true + true + true + true + + + + + sonatype + https://oss.sonatype.org/content/repositories/snapshots + + + + + + + cloud + + cloud + + + true + + + + + false + src/main/java + + **/*.java + + + + src/main/resources + + **/selfhost/application*.yml + + + + + + + selfhost + + selfhost + + + + + false + src/main/java + + **/*.java + + + + src/main/resources + + **/application*.yml + + + + + + + + + + + org.codehaus.mojo + license-maven-plugin + 2.0.0 + + + maven-dependency-plugin + + + + + + maven-assembly-plugin + 3.6.0 + + + src/assembly/bin.xml + + + + + + + + + + org.lowcoder lowcoder-sdk ${revision} - + org.lowcoder lowcoder-infra ${revision} - +
org.lowcoder lowcoder-domain ${revision} - +
org.lowcoder lowcoder-plugins ${revision} - +
org.lowcoder lowcoder-server ${revision} - - - - org.pf4j - pf4j - 3.5.0 - - - - org.json - json - 20230227 - - - - org.projectlombok - lombok - 1.18.26 - - - - org.apache.commons - commons-text - 1.10.0 - - - commons-io - commons-io - 2.13.0 - - - org.glassfish - javax.el - 3.0.0 - - - javax.el - javax.el-api - 3.0.0 - - - - org.eclipse.jgit - org.eclipse.jgit - 6.7.0.202309050840-r - - - - org.apache.commons - commons-collections4 - 4.4 - - - com.google.guava - guava - 30.0-jre - - - - tv.twelvetone.rjson - rjson - 1.3.1-SNAPSHOT - - - org.jetbrains.kotlin - kotlin-stdlib-jdk7 - 1.6.21 - - - - com.jayway.jsonpath - json-path - 2.7.0 - - - com.github.ben-manes.caffeine - caffeine - 3.0.5 - - - es.moki.ratelimitj - ratelimitj-core - 0.7.0 - - - com.github.spullara.mustache.java - compiler - 0.9.6 - - - - es.moki.ratelimitj - ratelimitj-redis - 0.7.0 - - - - io.projectreactor - reactor-core - 3.4.29 - - - - org.pf4j - pf4j-spring - 0.8.0 - - - - com.querydsl - querydsl-apt - 5.0.0 - - - - io.sentry - sentry-spring-boot-starter - 3.1.2 - - - - org.jgrapht - jgrapht-core - 1.5.0 - - - - javax.xml.bind - jaxb-api - 2.3.1 - - - javax.activation - activation - 1.1.1 - - - - org.glassfish.jaxb - jaxb-runtime - 2.3.3 - - - - com.github.cloudyrock.mongock - mongock-bom - 4.3.8 - pom - import - - - - io.projectreactor.tools - blockhound - 1.0.6.RELEASE - - - - jakarta.servlet - jakarta.servlet-api - 6.0.0 - - - io.projectreactor - reactor-test - 3.3.5.RELEASE - - - org.apache.httpcomponents - httpclient - 4.5.14 - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo.spring30x - 4.7.0 - - - org.mockito - mockito-inline - 5.2.0 - test - - - javax.validation - validation-api - 2.0.1.Final - - - - - - lowcoder-sdk - lowcoder-infra - lowcoder-domain - lowcoder-plugins - lowcoder-server - + + + + + lowcoder-dependencies + lowcoder-sdk + lowcoder-infra + lowcoder-domain + lowcoder-plugins + lowcoder-server + distribution +