From 345c18af60653a23b290039929d11f9543832d9e Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Thu, 29 Feb 2024 22:39:25 +0500 Subject: [PATCH 01/11] Label style work in progress --- .../src/components/Section.tsx | 1 + .../src/i18n/design/locales/en.ts | 1 + .../src/i18n/design/locales/zh.ts | 1 + .../comps/comps/textInputComp/inputComp.tsx | 32 ++++++----- .../textInputComp/textInputConstants.tsx | 4 +- .../src/comps/controls/labelControl.tsx | 55 ++++++++++++------- .../src/comps/controls/styleControl.tsx | 22 ++++++++ .../comps/controls/styleControlConstants.tsx | 28 ++++++++-- .../packages/lowcoder/src/i18n/locales/de.ts | 1 + .../packages/lowcoder/src/i18n/locales/en.ts | 1 + .../packages/lowcoder/src/i18n/locales/zh.ts | 1 + 11 files changed, 105 insertions(+), 42 deletions(-) diff --git a/client/packages/lowcoder-design/src/components/Section.tsx b/client/packages/lowcoder-design/src/components/Section.tsx index 869c0eabf..5b2d70b12 100644 --- a/client/packages/lowcoder-design/src/components/Section.tsx +++ b/client/packages/lowcoder-design/src/components/Section.tsx @@ -142,6 +142,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/src/comps/comps/textInputComp/inputComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx index fc34bc723..7c89af87f 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx @@ -1,7 +1,7 @@ import { Input, Section, sectionNames } from "lowcoder-design"; import { BoolControl } from "comps/controls/boolControl"; import { styleControl } from "comps/controls/styleControl"; -import { InputLikeStyle, InputLikeStyleType } from "comps/controls/styleControlConstants"; +import { InputLikeStyle, InputLikeStyleType, LabelStyle, LabelStyleType } from "comps/controls/styleControlConstants"; import { NameConfig, NameConfigPlaceHolder, @@ -38,7 +38,7 @@ import { EditorContext } from "comps/editorState"; * Input Comp */ -const InputStyle = styled(Input)<{ $style: InputLikeStyleType }>` +const InputStyle = styled(Input) <{ $style: InputLikeStyleType}>` ${(props) => props.$style && getStyle(props.$style)} `; @@ -48,6 +48,7 @@ const childrenMap = { showCount: BoolControl, allowClear: BoolControl, style: styleControl(InputLikeStyle), + labelStyle: styleControl(LabelStyle), prefixIcon: IconControl, suffixIcon: IconControl, }; @@ -67,7 +68,7 @@ export const InputComp = new UICompBuilder(childrenMap, (props) => { suffix={hasIcon(props.suffixIcon) && props.suffixIcon} /> ), - style: props.style, + style: props.labelStyle, ...validateState, }); }) @@ -80,22 +81,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()}
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 772cbdc51..6773a23f4 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"; @@ -217,7 +217,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}; diff --git a/client/packages/lowcoder/src/comps/controls/labelControl.tsx b/client/packages/lowcoder/src/comps/controls/labelControl.tsx index 74a4f26ac..a2fe0947e 100644 --- a/client/packages/lowcoder/src/comps/controls/labelControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/labelControl.tsx @@ -9,13 +9,13 @@ 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; @@ -75,10 +75,20 @@ const LabelWrapper = styled.div<{ max-width: ${(props) => (props.$position === "row" ? "80%" : "100%")}; flex-shrink: 0; `; - -const Label = styled.span<{ $border: boolean }>` +// ${(props) => props.$border && UnderlineCss}; +const Label = styled.span<{ $border: boolean, $labelStyle: LabelStyleType }>` ${labelCss}; - ${(props) => props.$border && UnderlineCss}; + + font-family:${(props) => props.$labelStyle.fontFamily}; + font-weight:${(props) => props.$labelStyle.fontWeight}; + 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.$labelStyle.text}; + ${(props) => props.$border && `border-bottom:${props.$labelStyle.borderWidth} ${props.$labelStyle.borderStyle} ${props.$labelStyle.border};`} + border-radius:${(props) => props.$labelStyle.radius}; + padding:${(props) => props.$labelStyle.padding}; width: fit-content; user-select: text; white-space: nowrap; @@ -144,21 +154,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 +225,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 109801a97..e9ad4b6d9 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControl.tsx @@ -550,6 +550,7 @@ export function styleControl(colorConfig name === "textTransform" || name === "textDecoration" || name === "fontFamily" || + name === "borderStyle" || name === "fontStyle" || name === "backgroundImage" || name === "backgroundImageRepeat" || @@ -688,6 +689,13 @@ export function styleControl(colorConfig label: config.label, preInputNode: , placeholder: props[name], + }): name === "borderStyle" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], }) : name === "margin" ? ( @@ -731,6 +739,20 @@ export function styleControl(colorConfig label: config.label, preInputNode: , placeholder: props[name], + }): name === "textDecoration" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], + }): name === "textTransform" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], }) : name === "fontStyle" ? ( diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index e36cd9d9b..334270500 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -55,6 +55,10 @@ export type FontStyleConfig = CommonColorConfig & { readonly fontStyle: string; } +export type borderStyleConfig = CommonColorConfig & { + readonly borderStyle: string; +} + export type ContainerHeaderPaddigConfig = CommonColorConfig & { readonly containerheaderpadding: string; }; @@ -88,7 +92,7 @@ export type DepColorConfig = CommonColorConfig & { readonly depType?: DEP_TYPE; transformer: (color: string, ...rest: string[]) => string; }; -export type SingleColorConfig = SimpleColorConfig | DepColorConfig | RadiusConfig | BorderWidthConfig | BackgroundImageConfig | BackgroundImageRepeatConfig | BackgroundImageSizeConfig | BackgroundImagePositionConfig | BackgroundImageOriginConfig | TextSizeConfig | TextWeightConfig | TextTransformConfig | TextDecorationConfig | FontFamilyConfig | FontStyleConfig | MarginConfig | PaddingConfig | ContainerHeaderPaddigConfig | ContainerFooterPaddigConfig | ContainerBodyPaddigConfig | HeaderBackgroundImageConfig | HeaderBackgroundImageRepeatConfig | HeaderBackgroundImageSizeConfig | HeaderBackgroundImagePositionConfig | HeaderBackgroundImageOriginConfig | FooterBackgroundImageConfig | FooterBackgroundImageRepeatConfig | FooterBackgroundImageSizeConfig | FooterBackgroundImagePositionConfig | FooterBackgroundImageOriginConfig; +export type SingleColorConfig = SimpleColorConfig | DepColorConfig | RadiusConfig | BorderWidthConfig | borderStyleConfig | BackgroundImageConfig | BackgroundImageRepeatConfig | BackgroundImageSizeConfig | BackgroundImagePositionConfig | BackgroundImageOriginConfig | TextSizeConfig | TextWeightConfig | TextTransformConfig | TextDecorationConfig | FontFamilyConfig | FontStyleConfig | MarginConfig | PaddingConfig | ContainerHeaderPaddigConfig | ContainerFooterPaddigConfig | ContainerBodyPaddigConfig | HeaderBackgroundImageConfig | HeaderBackgroundImageRepeatConfig | HeaderBackgroundImageSizeConfig | HeaderBackgroundImagePositionConfig | HeaderBackgroundImageOriginConfig | FooterBackgroundImageConfig | FooterBackgroundImageRepeatConfig | FooterBackgroundImageSizeConfig | FooterBackgroundImagePositionConfig | FooterBackgroundImageOriginConfig; export const defaultTheme: ThemeDetail = { primary: "#3377FF", @@ -399,13 +403,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) => ({ @@ -432,6 +442,7 @@ const STYLING_FIELDS_SEQUENCE = [ FONT_FAMILY, FONT_STYLE, BORDER, + BORDER_STYLE, MARGIN, PADDING, RADIUS, @@ -716,12 +727,16 @@ export const SliderStyle = [ ] as const; export const InputLikeStyle = [ - LABEL, + // LABEL, getStaticBackground(SURFACE_COLOR), ...STYLING_FIELDS_SEQUENCE, ...ACCENT_VALIDATE, ] as const; +export const LabelStyle = [ + ...replaceAndMergeMultipleStyles([...InputLikeStyle], 'text', [LABEL]).filter((style) => style.name !== 'radius') +] + export const RatingStyle = [ LABEL, { @@ -800,7 +815,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", @@ -888,7 +903,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"), @@ -1361,6 +1376,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/i18n/locales/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index 404520788..dbe3f32e4 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 a01b2c5e9..75985f8c7 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -316,6 +316,7 @@ export const en = { "border": "Border Color", "borderRadius": "Border Radius", "borderWidth": "Border Width", + "borderStyle":"Border Style", "background": "Background", "headerBackground": "Header Background", "footerBackground": "Footer Background", diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index ca3e99f49..d40474c2a 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -307,6 +307,7 @@ style: { border: "边框颜色", borderRadius: "边框半径", borderWidth: "边框宽度", + borderStyle:"边框样式", background: "背景", headerBackground: "头部背景", footerBackground: "底部背景", From 1f164533ea486e9128094289b9dcb0f4a72b4f23 Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Fri, 1 Mar 2024 22:56:13 +0500 Subject: [PATCH 02/11] Label style w.i.p --- .../autoCompleteComp/autoCompleteComp.tsx | 9 ++++-- .../comps/numberInputComp/numberInputComp.tsx | 10 ++++-- .../comps/numberInputComp/rangeSliderComp.tsx | 2 +- .../comps/numberInputComp/sliderComp.tsx | 2 +- .../numberInputComp/sliderCompConstants.tsx | 6 +++- .../comps/selectInputComp/cascaderComp.tsx | 2 +- .../selectInputComp/cascaderContants.tsx | 18 +++++++---- .../comps/selectInputComp/checkboxComp.tsx | 5 +-- .../comps/selectInputComp/multiSelectComp.tsx | 5 +-- .../comps/comps/selectInputComp/radioComp.tsx | 2 +- .../selectInputComp/radioCompConstants.tsx | 6 +++- .../comps/selectInputComp/selectComp.tsx | 7 ++-- .../selectInputComp/selectCompConstants.tsx | 14 +++++--- .../lowcoder/src/comps/comps/switchComp.tsx | 16 +++++++--- .../comps/textInputComp/passwordComp.tsx | 30 +++++++++-------- .../comps/textInputComp/textAreaComp.tsx | 32 +++++++++++-------- .../src/comps/comps/treeComp/treeComp.tsx | 16 ++++++---- .../comps/comps/treeComp/treeSelectComp.tsx | 13 ++++---- .../src/comps/controls/labelControl.tsx | 8 ++--- .../comps/controls/styleControlConstants.tsx | 6 ++-- 20 files changed, 131 insertions(+), 78 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx index 059dd3852..d052bf4cb 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,8 @@ let AutoCompleteCompBase = (function () { ), - // style: props.style, - // ...validateState, + style: props.labelStyle, + ...validateState, }); }) .setPropertyViewFn((children) => { @@ -335,6 +337,9 @@ let AutoCompleteCompBase = (function () {
{children.style.getPropertyView()}
+
+ {children.labelStyle.getPropertyView()} +
); }) diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx index cb5a7836e..4447ddf2d 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, @@ -256,6 +256,7 @@ const childrenMap = { onEvent: InputEventHandlerControl, viewRef: RefControl, style: styleControl(InputLikeStyle), + labelStyle:styleControl(LabelStyle), prefixIcon: IconControl, // validation @@ -377,7 +378,7 @@ const NumberInputTmpComp = (function () { return props.label({ required: props.required, children: , - style: props.style, + style: props.labelStyle, ...validate(props), }); }) @@ -425,9 +426,14 @@ const NumberInputTmpComp = (function () { )} {(useContext(EditorContext).editorModeStatus === "layout" || useContext(EditorContext).editorModeStatus === "both") && ( + <>
{children.style.getPropertyView()}
+
+ {children.labelStyle.getPropertyView()} +
+ )} )) diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx index bea60397c..a3ff568fe 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx @@ -14,7 +14,7 @@ const RangeSliderBasicComp = (function () { }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.style, + style: props.labelStyle, children: ( { diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx index b86b07ef6..4279588a8 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx @@ -18,7 +18,7 @@ const SliderBasicComp = (function () { }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.style, + style: props.labelStyle, children: ( { diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderCompConstants.tsx index 5e7760acb..601a72c0d 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"; @@ -67,6 +67,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 +97,9 @@ export const SliderPropertyView = (
{children.style.getPropertyView()}
+
+ {children.labelStyle.getPropertyView()} +
)} diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx index e89c0e49f..10bc5d5b0 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/cascaderComp.tsx @@ -20,7 +20,7 @@ let CascaderBasicComp = (function () { return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.style, + style: 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..c3e20a636 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx @@ -15,7 +15,7 @@ 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"; @@ -135,6 +135,7 @@ const CheckboxBasicComp = (function () { onEvent: ChangeEventHandlerControl, options: SelectInputOptionControl, style: styleControl(CheckboxStyle), + labelStyle:styleControl(LabelStyle), layout: dropdownControl(RadioLayoutOptions, "horizontal"), viewRef: RefControl, @@ -148,7 +149,7 @@ const CheckboxBasicComp = (function () { ] = useSelectInputValidate(props); return props.label({ required: props.required, - style: props.style, + style: 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/selectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx index 1a30f2522..644c3c958 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"; @@ -24,6 +24,7 @@ const SelectBasicComp = (function () { defaultValue: stringExposingStateControl("defaultValue"), value: stringExposingStateControl("value"), style: styleControl(SelectStyle), + labelStyle: styleControl(LabelStyle) }; return new UICompBuilder(childrenMap, (props, dispatch) => { const [ @@ -35,10 +36,10 @@ 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, + style: 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/switchComp.tsx b/client/packages/lowcoder/src/comps/comps/switchComp.tsx index 3a3ecf955..03e164072 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,13 +90,14 @@ let SwitchTmpComp = (function () { onEvent: eventHandlerControl(EventOptions), disabled: BoolCodeControl, style: migrateOldData(styleControl(SwitchStyle), fixOldData), + labelStyle: styleControl(LabelStyle), viewRef: RefControl, ...formDataChildren, }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.style, + style: props.labelStyle, children: ( - {children.style.getPropertyView()} - + <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx index b5c3d701d..ff0ab797a 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx @@ -25,7 +25,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, @@ -41,7 +41,7 @@ import { RefControl } from "comps/controls/refControl"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; -const PasswordStyle = styled(InputPassword)<{ +const PasswordStyle = styled(InputPassword) <{ $style: InputLikeStyleType; }>` ${(props) => props.$style && getStyle(props.$style)} @@ -56,6 +56,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); @@ -70,7 +71,7 @@ const PasswordTmpComp = (function () { $style={props.style} /> ), - style: props.style, + style: props.labelStyle, ...validateState, }); }) @@ -86,24 +87,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()}
+ )} ); diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx index e11027a69..a7a155e84 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx @@ -21,7 +21,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, @@ -36,7 +36,7 @@ import { blurMethod, focusWithOptions } from "comps/utils/methodUtils"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; -const TextAreaStyled = styled(TextArea)<{ +const TextAreaStyled = styled(TextArea) <{ $style: InputLikeStyleType; }>` ${(props) => props.$style && getStyle(props.$style)} @@ -70,6 +70,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 +78,7 @@ let TextAreaTmpComp = (function () { required: props.required, children: ( - ), - style: props.style, + style: props.labelStyle, ...validateState, }); }) @@ -101,19 +102,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()}
+ )} )) diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx index 6474efa9b..06b3b9040 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,7 @@ const TreeCompView = (props: RecordConstructorToView) => { return props.label({ required: props.required, ...selectInputValidate(props), - style: style, + style: labelStyle, children: ( setHeight(h)}> @@ -166,7 +167,7 @@ let TreeBasicComp = (function () { )} - + {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && (
{children.expanded.propertyView({ label: trans("tree.expanded") })} @@ -176,10 +177,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..7db2d9e4c 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,7 @@ 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/controls/labelControl.tsx b/client/packages/lowcoder/src/comps/controls/labelControl.tsx index a2fe0947e..48233b318 100644 --- a/client/packages/lowcoder/src/comps/controls/labelControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/labelControl.tsx @@ -76,17 +76,16 @@ const LabelWrapper = styled.div<{ flex-shrink: 0; `; // ${(props) => props.$border && UnderlineCss}; -const Label = styled.span<{ $border: boolean, $labelStyle: LabelStyleType }>` +const Label = styled.span<{ $border: boolean, $labelStyle: LabelStyleType, $validateStatus: "success" | "warning" | "error" | "validating" | null }>` ${labelCss}; - font-family:${(props) => props.$labelStyle.fontFamily}; font-weight:${(props) => props.$labelStyle.fontWeight}; 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.$labelStyle.text}; - ${(props) => props.$border && `border-bottom:${props.$labelStyle.borderWidth} ${props.$labelStyle.borderStyle} ${props.$labelStyle.border};`} + color:${(props) => !!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.text} !important; + ${(props) => props.$border && `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}; width: fit-content; @@ -194,6 +193,7 @@ export const LabelControl = (function () { > diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 4d6d09d0e..3ca944330 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -728,7 +728,7 @@ export const InputLikeStyle = [ ] as const; export const LabelStyle = [ - ...replaceAndMergeMultipleStyles([...InputLikeStyle], 'text', [LABEL]).filter((style) => style.name !== 'radius') + ...replaceAndMergeMultipleStyles([...InputLikeStyle], 'text', [LABEL]).filter((style) => style.name !== 'radius' && style.name !== 'background') ] export const RatingStyle = [ @@ -1153,11 +1153,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; From 15ee23458c7900f37b9e7c27e1d463551a7ef3c8 Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Mon, 4 Mar 2024 20:35:37 +0500 Subject: [PATCH 03/11] Rating component, signature component ,date component label styling panel and properties added along with default values of latest add CSS properties --- .../src/comps/comps/dateComp/dateComp.tsx | 65 +++++--- .../lowcoder/src/comps/comps/ratingComp.tsx | 32 ++-- .../src/comps/comps/signatureComp.tsx | 23 ++- .../src/comps/controls/styleControl.tsx | 154 +++++++++++------- .../comps/controls/styleControlConstants.tsx | 12 +- 5 files changed, 172 insertions(+), 114 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx index ceeb9a257..2d5030132 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,12 @@ 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, + style: 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 +235,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 +245,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 +270,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); } @@ -309,13 +315,13 @@ export const dateRangeControl = (function () { return props.label({ required: props.required, - style: props.style, + style: props.labelStyle, children: children, ...(startResult.validateStatus !== "success" ? startResult : endResult.validateStatus !== "success" - ? endResult - : startResult), + ? endResult + : startResult), }); }) .setPropertyViewFn((children) => { @@ -337,11 +343,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 +364,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 +374,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/ratingComp.tsx b/client/packages/lowcoder/src/comps/comps/ratingComp.tsx index 40db580ab..a0504d081 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) => { @@ -63,7 +64,7 @@ const RatingBasicComp = (function () { }, [value]); return props.label({ - style: props.style, + style: 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 +111,14 @@ const RatingBasicComp = (function () { )} {["layout", "both"].includes(useContext(EditorContext).editorModeStatus) && ( -
- {children.style.getPropertyView()} -
+ <> +
+ {children.style.getPropertyView()} +
+
+ {children.labelStyle.getPropertyView()} +
+ )} ); @@ -144,6 +150,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/signatureComp.tsx b/client/packages/lowcoder/src/comps/comps/signatureComp.tsx index 8db397915..0f16be8df 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(""), @@ -125,7 +127,7 @@ let SignatureTmpComp = (function () { } }; return props.label({ - style: props.style, + style: props.labelStyle, children: ( { @@ -218,9 +220,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/controls/styleControl.tsx b/client/packages/lowcoder/src/comps/controls/styleControl.tsx index e9ad4b6d9..880b1a69d 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]; } @@ -689,126 +731,126 @@ export function styleControl(colorConfig label: config.label, preInputNode: , placeholder: props[name], - }): name === "borderStyle" - ? ( - children[name] as InstanceType - ).propertyView({ - 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" + : (name === "padding" || + name === "containerheaderpadding" || + name === "containerfooterpadding" || + name === "containerbodypadding") ? ( 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: , - placeholder: props[name], - }): name === "textDecoration" - ? ( - children[name] as InstanceType - ).propertyView({ - label: config.label, - preInputNode: , - placeholder: props[name], - }): name === "textTransform" - ? ( - 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({ - label: config.label, - panelDefaultColor: props[name], - // isDep: isDepColorConfig(config), - isDep: true, - depMsg: depMsg, - })} + : name === "backgroundImageSize" || name === "headerBackgroundImageSize" || name === "footerBackgroundImageSize" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], + }) + : name === "backgroundImagePosition" || name === "headerBackgroundImagePosition" || name === "footerBackgroundImagePosition" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], + }) + : name === "backgroundImageOrigin" || name === "headerBackgroundImageOrigin" || name === "footerBackgroundImageOrigin" + ? ( + children[name] as InstanceType + ).propertyView({ + label: config.label, + preInputNode: , + placeholder: props[name], + }) + : children[name].propertyView({ + label: config.label, + panelDefaultColor: props[name], + // isDep: isDepColorConfig(config), + isDep: true, + depMsg: depMsg, + })} ); })} diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx index 3ca944330..5aeec739c 100644 --- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx +++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx @@ -701,7 +701,6 @@ export const ContainerFooterStyle = [ ] as const; export const SliderStyle = [ - LABEL, FILL, { name: "thumbBoder", @@ -721,7 +720,6 @@ export const SliderStyle = [ ] as const; export const InputLikeStyle = [ - // LABEL, getStaticBackground(SURFACE_COLOR), ...STYLING_FIELDS_SEQUENCE, ...ACCENT_VALIDATE, @@ -732,7 +730,6 @@ export const LabelStyle = [ ] export const RatingStyle = [ - LABEL, { name: "checked", label: trans("style.checked"), @@ -748,7 +745,6 @@ export const RatingStyle = [ ] as const; export const SwitchStyle = [ - LABEL, { name: "handle", label: trans("style.handle"), @@ -838,7 +834,6 @@ export const ModalStyle = [ ] as const; export const CascaderStyle = [ - LABEL, ...getStaticBgBorderRadiusByBg(SURFACE_COLOR, "pc"), TEXT, ACCENT, @@ -870,7 +865,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", @@ -883,7 +878,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", @@ -1055,7 +1050,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, @@ -1202,7 +1196,6 @@ export const TimeLineStyle = [ ] as const; export const TreeStyle = [ - LABEL, ...getStaticBgBorderRadiusByBg(SURFACE_COLOR), TEXT, VALIDATE, @@ -1259,7 +1252,6 @@ export const CalendarStyle = [ ] as const; export const SignatureStyle = [ - LABEL, ...getBgBorderRadiusByBg(), { name: "pen", From e9a17d7735b01529065e948e679362385e3a88dd Mon Sep 17 00:00:00 2001 From: Imtanan Aziz Toor Date: Thu, 7 Mar 2024 17:33:27 +0500 Subject: [PATCH 04/11] Fixed issue when apply bulk margin and padding --- .../lowcoder-design/src/components/Label.tsx | 1 - .../comps/autoCompleteComp/autoCompleteComp.tsx | 3 ++- .../lowcoder/src/comps/comps/dateComp/dateComp.tsx | 6 ++++-- .../comps/comps/numberInputComp/numberInputComp.tsx | 3 ++- .../comps/comps/numberInputComp/rangeSliderComp.tsx | 5 +++-- .../src/comps/comps/numberInputComp/sliderComp.tsx | 3 ++- .../packages/lowcoder/src/comps/comps/ratingComp.tsx | 3 ++- .../src/comps/comps/selectInputComp/cascaderComp.tsx | 3 ++- .../src/comps/comps/selectInputComp/checkboxComp.tsx | 9 +++++---- .../comps/comps/selectInputComp/multiSelectComp.tsx | 3 ++- .../src/comps/comps/selectInputComp/radioComp.tsx | 3 ++- .../src/comps/comps/selectInputComp/selectComp.tsx | 3 ++- .../lowcoder/src/comps/comps/signatureComp.tsx | 3 ++- .../packages/lowcoder/src/comps/comps/switchComp.tsx | 5 +++-- .../src/comps/comps/textInputComp/inputComp.tsx | 5 +++-- .../src/comps/comps/textInputComp/passwordComp.tsx | 3 ++- .../src/comps/comps/textInputComp/textAreaComp.tsx | 5 +++-- .../comps/comps/textInputComp/textInputConstants.tsx | 2 +- .../lowcoder/src/comps/comps/treeComp/treeComp.tsx | 3 ++- .../src/comps/comps/treeComp/treeSelectComp.tsx | 3 ++- .../lowcoder/src/comps/controls/labelControl.tsx | 12 ++++++++---- 21 files changed, 54 insertions(+), 32 deletions(-) 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/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx index d052bf4cb..ab1a992fa 100644 --- a/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/autoCompleteComp/autoCompleteComp.tsx @@ -278,7 +278,8 @@ let AutoCompleteCompBase = (function () { ), - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx index 2d5030132..2977a60e2 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx @@ -165,7 +165,8 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { } return props.label({ required: props.required, - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, children: ( , - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, ...validate(props), }); }) diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx index a3ff568fe..7deb69530 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/rangeSliderComp.tsx @@ -14,7 +14,8 @@ const RangeSliderBasicComp = (function () { }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.labelStyle, + 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 4279588a8..23181e5d8 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/sliderComp.tsx @@ -18,7 +18,8 @@ const SliderBasicComp = (function () { }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, children: ( { diff --git a/client/packages/lowcoder/src/comps/comps/ratingComp.tsx b/client/packages/lowcoder/src/comps/comps/ratingComp.tsx index a0504d081..df93e688c 100644 --- a/client/packages/lowcoder/src/comps/comps/ratingComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/ratingComp.tsx @@ -64,7 +64,8 @@ const RatingBasicComp = (function () { }, [value]); return props.label({ - style: props.labelStyle, + style: props.style, + labelStyle: props.labelStyle, children: ( { return props.label({ - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, children: ( { &: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, @@ -135,7 +135,7 @@ const CheckboxBasicComp = (function () { onEvent: ChangeEventHandlerControl, options: SelectInputOptionControl, style: styleControl(CheckboxStyle), - labelStyle:styleControl(LabelStyle), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)), layout: dropdownControl(RadioLayoutOptions, "horizontal"), viewRef: RefControl, @@ -149,7 +149,8 @@ const CheckboxBasicComp = (function () { ] = useSelectInputValidate(props); return props.label({ required: props.required, - style: props.labelStyle, + style: props.style, + labelStyle: props.labelStyle, children: ( { diff --git a/client/packages/lowcoder/src/comps/comps/switchComp.tsx b/client/packages/lowcoder/src/comps/comps/switchComp.tsx index 03e164072..70c6a4379 100644 --- a/client/packages/lowcoder/src/comps/comps/switchComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/switchComp.tsx @@ -90,14 +90,15 @@ let SwitchTmpComp = (function () { onEvent: eventHandlerControl(EventOptions), disabled: BoolCodeControl, style: migrateOldData(styleControl(SwitchStyle), fixOldData), - labelStyle: styleControl(LabelStyle), + labelStyle: styleControl(LabelStyle.filter((style) => ['accent', 'validate'].includes(style.name) === false)), viewRef: RefControl, ...formDataChildren, }; return new UICompBuilder(childrenMap, (props) => { return props.label({ - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, children: ( ` +const InputStyle = styled(Input) <{ $style: InputLikeStyleType }>` ${(props) => props.$style && getStyle(props.$style)} `; @@ -68,7 +68,8 @@ export const InputComp = new UICompBuilder(childrenMap, (props) => { suffix={hasIcon(props.suffixIcon) && props.suffixIcon} /> ), - style: props.labelStyle, + style: props.style, + labelStyle: props.labelStyle, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx index ff0ab797a..2a3a23f67 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx @@ -71,7 +71,8 @@ const PasswordTmpComp = (function () { $style={props.style} /> ), - style: props.labelStyle, + style: props.style, + labelStyle:props.labelStyle, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx index a7a155e84..a47028235 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx @@ -46,7 +46,7 @@ const Wrapper = styled.div<{ $style: InputLikeStyleType; }>` height: 100% !important; - + .ant-input { height:100% !important; } @@ -87,7 +87,8 @@ let TextAreaTmpComp = (function () { /> ), - style: props.labelStyle, + style: props.style, + labelStyle: props.labelStyle, ...validateState, }); }) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 6f244ef2c..cfbe94c41 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -250,7 +250,7 @@ export function getStyle(style: InputLikeStyleType, labelStyle?: LabelStyleType) 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, diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx index 06b3b9040..753df6688 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeComp.tsx @@ -96,7 +96,8 @@ const TreeCompView = (props: RecordConstructorToView) => { return props.label({ required: props.required, ...selectInputValidate(props), - style: labelStyle, + style, + labelStyle, children: ( setHeight(h)}> diff --git a/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx b/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx index 7db2d9e4c..de15121b3 100644 --- a/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/treeComp/treeSelectComp.tsx @@ -100,7 +100,8 @@ const TreeCompView = ( return props.label({ required: props.required, ...validateState, - style: labelStyle, + style, + labelStyle, children: ( & { children: ReactNode; style?: Record; + labelStyle?: Record; }; const StyledStarIcon = styled(StarIcon)` @@ -76,18 +77,21 @@ const LabelWrapper = styled.div<{ 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, $labelStyle: LabelStyleType, $validateStatus: "success" | "warning" | "error" | "validating" | null }>` ${labelCss}; font-family:${(props) => props.$labelStyle.fontFamily}; - font-weight:${(props) => props.$labelStyle.fontWeight}; + 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.text} !important; - ${(props) => props.$border && `border-bottom:${props.$labelStyle.borderWidth} ${props.$labelStyle.borderStyle} ${!!props.$validateStatus && props?.$validateStatus === 'error' ? props.$labelStyle.validate : props.$labelStyle.border};`} + 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; @@ -194,7 +198,7 @@ export const LabelControl = (function () { From eb0e0d5a07b459e3711c8cfebedbf8b4a5da27ae Mon Sep 17 00:00:00 2001 From: Muhammad Irfan Ayub Date: Wed, 6 Mar 2024 21:39:03 +0500 Subject: [PATCH 05/11] feature: Extended folder to have more meta-data i.e: title, description, category, type, image --- .../java/org/lowcoder/domain/folder/model/Folder.java | 5 +++++ .../java/org/lowcoder/api/home/FolderApiService.java | 9 +++++++++ .../main/java/org/lowcoder/api/home/FolderInfoView.java | 5 +++++ 3 files changed, 19 insertions(+) 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-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..d5fc1a4eb 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())) @@ -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/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; From d1e6f9241129afaf5728b6c3f72755c456cc470a Mon Sep 17 00:00:00 2001 From: "Tyler B. Thrailkill" Date: Fri, 8 Mar 2024 16:10:37 -0700 Subject: [PATCH 06/11] Optimize the runOpenApi plugin allow passing in an already dereferenced OpenAPI Document for plugins that have large openapi specs like github. This takes the runtime for Github from over 10s to ~1s on an M1 Max. Adjust the github plugin to use this new capability by dereferencing on plugin initialization, passing the derefed document to the runOpenApi function. --- .../node-service/src/plugins/github/index.ts | 6 ++-- .../node-service/src/plugins/openApi/index.ts | 30 ++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/server/node-service/src/plugins/github/index.ts b/server/node-service/src/plugins/github/index.ts index 5df537677..3f031b65e 100644 --- a/server/node-service/src/plugins/github/index.ts +++ b/server/node-service/src/plugins/github/index.ts @@ -5,6 +5,7 @@ import { OpenAPIV3, OpenAPI } from "openapi-types"; import { ConfigToType, DataSourcePlugin } from "lowcoder-sdk/dataSource"; import { runOpenApi } from "../openApi"; import { parseOpenApi, ParseOpenApiOptions } from "../openApi/parse"; +import SwaggerParser from "@apidevtools/swagger-parser"; const spec = readYaml(path.join(__dirname, "./github.spec.yaml")); @@ -34,6 +35,7 @@ const parseOptions: ParseOpenApiOptions = { return _.upperFirst(operation.operationId || ""); }, }; +const deRefedSpec = SwaggerParser.dereference(spec); type DataSourceConfigType = ConfigToType; @@ -55,13 +57,13 @@ const gitHubPlugin: DataSourcePlugin = { actions, }; }, - run: function (actionData, dataSourceConfig): Promise { + run: async function (actionData, dataSourceConfig, ctx): Promise { const runApiDsConfig = { url: "", serverURL: "https://api.github.com", dynamicParamsConfig: dataSourceConfig, }; - return runOpenApi(actionData, runApiDsConfig, spec as OpenAPIV3.Document); + return runOpenApi(actionData, runApiDsConfig, spec as OpenAPIV3.Document, undefined, await deRefedSpec); }, }; diff --git a/server/node-service/src/plugins/openApi/index.ts b/server/node-service/src/plugins/openApi/index.ts index 27f9ed950..eaec12aac 100644 --- a/server/node-service/src/plugins/openApi/index.ts +++ b/server/node-service/src/plugins/openApi/index.ts @@ -54,18 +54,28 @@ export async function runOpenApi( actionData: ActionDataType, dataSourceConfig: DataSourceDataType, spec: OpenAPI.Document | MultiOpenApiSpecItem[], - defaultHeaders?: Record + defaultHeaders?: Record, + openApiSpecDereferenced?: OpenAPI.Document, ) { const specList = Array.isArray(spec) ? spec : [{ spec, id: "" }]; - const definitions = await Promise.all( - specList.map(async ({ id, spec }) => { - const deRefedSpec = await SwaggerParser.dereference(spec); - return { - def: deRefedSpec, - id, - }; - }) - ); + let definitions; + + if (!openApiSpecDereferenced) { + definitions = await Promise.all( + specList.map(async ({id, spec}) => { + const deRefedSpec = await SwaggerParser.dereference(spec); + return { + def: deRefedSpec, + id, + }; + }) + ); + } else { + definitions = [{ + def: openApiSpecDereferenced, + id: "", + }] + } const { actionName, ...otherActionData } = actionData; const { serverURL } = dataSourceConfig; From ed4f40be6df258dc81b5e38ec5fbdfe09deff288 Mon Sep 17 00:00:00 2001 From: "Tyler B. Thrailkill" Date: Fri, 8 Mar 2024 16:15:44 -0700 Subject: [PATCH 07/11] Pull definition logic into a new function --- .../node-service/src/plugins/openApi/index.ts | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/server/node-service/src/plugins/openApi/index.ts b/server/node-service/src/plugins/openApi/index.ts index eaec12aac..265b23b44 100644 --- a/server/node-service/src/plugins/openApi/index.ts +++ b/server/node-service/src/plugins/openApi/index.ts @@ -50,18 +50,18 @@ interface ActionDataType { [key: string]: any; } -export async function runOpenApi( - actionData: ActionDataType, - dataSourceConfig: DataSourceDataType, +async function getDefinitions( spec: OpenAPI.Document | MultiOpenApiSpecItem[], - defaultHeaders?: Record, openApiSpecDereferenced?: OpenAPI.Document, -) { - const specList = Array.isArray(spec) ? spec : [{ spec, id: "" }]; - let definitions; - - if (!openApiSpecDereferenced) { - definitions = await Promise.all( +): Promise<{ def: OpenAPI.Document; id: string }[]> { + if (openApiSpecDereferenced) { + return [{ + def: openApiSpecDereferenced, + id: "", + }] + } else { + const specList = Array.isArray(spec) ? spec : [{ spec, id: "" }]; + return await Promise.all( specList.map(async ({id, spec}) => { const deRefedSpec = await SwaggerParser.dereference(spec); return { @@ -70,20 +70,22 @@ export async function runOpenApi( }; }) ); - } else { - definitions = [{ - def: openApiSpecDereferenced, - id: "", - }] } +} + +export async function runOpenApi( + actionData: ActionDataType, + dataSourceConfig: DataSourceDataType, + spec: OpenAPI.Document | MultiOpenApiSpecItem[], + defaultHeaders?: Record, + openApiSpecDereferenced?: OpenAPI.Document, +) { const { actionName, ...otherActionData } = actionData; const { serverURL } = dataSourceConfig; - let definition: OpenAPI.Document | undefined; - let operation; - let realOperationId; + let operation, realOperationId, definition: OpenAPI.Document | undefined; - for (const { id, def } of definitions) { + for (const {id, def} of await getDefinitions(spec, openApiSpecDereferenced)) { const ret = findOperation(actionName, def, id); if (ret) { definition = def; From 275cf68eb73e80a43034e5d9a88431949cfe6f0b Mon Sep 17 00:00:00 2001 From: "Tyler B. Thrailkill" Date: Fri, 8 Mar 2024 16:16:39 -0700 Subject: [PATCH 08/11] Small amount of code cleanup --- server/node-service/src/plugins/openApi/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/node-service/src/plugins/openApi/index.ts b/server/node-service/src/plugins/openApi/index.ts index 265b23b44..c99bcc9a3 100644 --- a/server/node-service/src/plugins/openApi/index.ts +++ b/server/node-service/src/plugins/openApi/index.ts @@ -12,11 +12,10 @@ import { isFile, } from "./util"; import { badRequest } from "../../common/error"; -import { safeJsonParse } from "../../common/util"; import { OpenAPI, OpenAPIV2, OpenAPIV3 } from "openapi-types"; import _ from "lodash"; import { fetch } from "../../common/fetch"; -import { RequestInit, Response } from "node-fetch"; +import { RequestInit } from "node-fetch"; const dataSourceConfig = { type: "dataSource", From 488e2342d316e4dbb85d6fec5843a45a5ec98750 Mon Sep 17 00:00:00 2001 From: Ludovit Mikula Date: Mon, 5 Feb 2024 10:38:30 +0100 Subject: [PATCH 09/11] New plugin system * Add support for SUPER_ADMIN role * Publish server log events * Add handling for audit logs feature * Add handling for geolocation data * Add handling for api delays in case of rate limit * Propagate plugin specific environment variables to plugins * Add environment variable for controlling plugin location * Implemented plugin endpoints security --------- Co-authored-by: Abdul Qadir --- .gitignore | 1 + deploy/docker/Dockerfile | 20 +- deploy/docker/api-service/entrypoint.sh | 6 +- server/api-service/.gitignore | 6 +- server/api-service/PLUGIN.md | 122 ++++ server/api-service/distribution/pom.xml | 84 +++ .../distribution/src/assembly/bin.xml | 72 ++ .../src/assembly/set-classpath.sh | 11 + .../api-service/lowcoder-dependencies/pom.xml | 224 +++++++ server/api-service/lowcoder-domain/pom.xml | 36 +- .../configurations/Pf4jConfiguration.java | 15 - .../domain/group/model/GroupMember.java | 5 + .../domain/organization/model/MemberRole.java | 3 +- .../domain/organization/model/OrgMember.java | 4 + .../service/OrganizationService.java | 4 +- .../service/OrganizationServiceImpl.java | 18 +- .../service/ResourcePermissionHandler.java | 4 +- .../domain/user/service/UserServiceImpl.java | 2 +- server/api-service/lowcoder-infra/pom.xml | 19 + .../lowcoder/infra/event/APICallEvent.java | 21 + .../lowcoder/infra/event/AbstractEvent.java | 48 +- .../java/org/lowcoder/infra/event/Event.java | 6 - .../org/lowcoder/infra/event/EventType.java | 63 -- .../infra/event/SystemCommonEvent.java | 18 + .../event/datasource/DatasourceEvent.java | 1 - .../datasource/DatasourcePermissionEvent.java | 1 - .../infra/event/group/GroupCreateEvent.java | 2 - .../infra/event/group/GroupDeleteEvent.java | 2 - .../infra/event/group/GroupUpdateEvent.java | 2 - .../groupmember/GroupMemberAddEvent.java | 2 - .../groupmember/GroupMemberLeaveEvent.java | 2 - .../groupmember/GroupMemberRemoveEvent.java | 2 - .../GroupMemberRoleUpdateEvent.java | 2 - .../infra/event/user/UserLoginEvent.java | 1 - .../infra/event/user/UserLogoutEvent.java | 1 - .../infra/localcache/ReloadableCache.java | 2 +- .../infra/serverlog/ServerLogService.java | 11 + .../clickHousePlugin/plugin.properties | 5 - .../elasticSearchPlugin/plugin.properties | 5 - .../googleSheetsPlugin/plugin.properties | 5 - .../graphqlPlugin/plugin.properties | 5 - .../lowcoderApiPlugin/plugin.properties | 5 - .../mongoPlugin/plugin.properties | 5 - .../mssqlPlugin/plugin.properties | 5 - .../mysqlPlugin/plugin.properties | 5 - .../oraclePlugin/plugin.properties | 5 - .../lowcoder-plugins/oraclePlugin/pom.xml | 3 + server/api-service/lowcoder-plugins/pom.xml | 8 + .../postgresPlugin/plugin.properties | 5 - .../redisPlugin/plugin.properties | 5 - .../restApiPlugin/plugin.properties | 5 - .../smtpPlugin/plugin.properties | 5 - .../snowflakePlugin/plugin.properties | 5 - server/api-service/lowcoder-sdk/pom.xml | 17 +- .../org/lowcoder/sdk/config/CommonConfig.java | 8 + .../api-service/lowcoder-server/cert/README | 33 + .../lowcoder-server/cert/signing.p12 | Bin 0 -> 4434 bytes server/api-service/lowcoder-server/pom.xml | 623 ++++++++++-------- .../src/main/assembly/assembly.xml | 58 ++ .../org/lowcoder/api/ServerApplication.java | 3 + .../application/ApplicationController.java | 14 +- .../service/AuthenticationApiServiceImpl.java | 10 +- .../api/datasource/DatasourceController.java | 12 +- .../ApplicationConfiguration.java | 15 + .../CustomWebFluxConfigurationSupport.java | 16 + .../configuration/PluginConfiguration.java | 58 ++ .../api/framework/filter/APIDelayFilter.java | 38 ++ .../api/framework/filter/FilterOrder.java | 2 + .../filter/ReactiveRequestContextFilter.java | 18 + .../filter/ReactiveRequestContextHolder.java | 13 + .../framework/filter/ThrottlingFilter.java | 2 +- .../plugin/LowcoderPluginManager.java | 130 ++++ .../plugin/PathBasedPluginLoader.java | 140 ++++ .../framework/plugin/PluginClassLoader.java | 104 +++ .../api/framework/plugin/PluginExecutor.java | 36 + .../api/framework/plugin/PluginLoader.java | 11 + .../plugin/SharedPluginServices.java | 59 ++ .../plugin/data/PluginServerRequest.java | 198 ++++++ .../endpoint/PluginEndpointHandler.java | 15 + .../endpoint/PluginEndpointHandlerImpl.java | 195 ++++++ .../ReloadableRouterFunctionMapping.java | 20 + .../security/PluginAuthorizationManager.java | 94 +++ .../framework/security/SecurityConfig.java | 78 ++- .../lowcoder/api/home/FolderApiService.java | 4 +- .../lowcoder/api/home/FolderController.java | 8 +- .../lowcoder/api/home/SessionUserService.java | 4 + .../api/home/SessionUserServiceImpl.java | 12 + .../api/query/LibraryQueryController.java | 2 +- .../api/usermanagement/GroupApiService.java | 17 +- .../api/usermanagement/OrgApiServiceImpl.java | 2 +- .../api/usermanagement/OrgDevChecker.java | 2 +- .../api/usermanagement/UserApiService.java | 2 +- .../api/util/ApiCallEventPublisher.java | 90 +++ .../api/util/BusinessEventPublisher.java | 171 +++-- .../util/RandomPasswordGeneratorConfig.java | 28 + .../runner/migrations/DatabaseChangelog.java | 8 +- .../migrations/job/AddSuperAdminUser.java | 6 + .../migrations/job/AddSuperAdminUserImpl.java | 67 ++ .../main/resources/application-lowcoder.yml | 12 + .../resources/selfhost/ce/application.yml | 4 +- server/api-service/pom.xml | 441 ++++--------- 101 files changed, 2919 insertions(+), 905 deletions(-) create mode 100644 server/api-service/PLUGIN.md create mode 100644 server/api-service/distribution/pom.xml create mode 100644 server/api-service/distribution/src/assembly/bin.xml create mode 100755 server/api-service/distribution/src/assembly/set-classpath.sh create mode 100644 server/api-service/lowcoder-dependencies/pom.xml delete mode 100644 server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java create mode 100644 server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/APICallEvent.java delete mode 100644 server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/Event.java delete mode 100644 server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/EventType.java create mode 100644 server/api-service/lowcoder-infra/src/main/java/org/lowcoder/infra/event/SystemCommonEvent.java delete mode 100644 server/api-service/lowcoder-plugins/clickHousePlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/elasticSearchPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/googleSheetsPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/graphqlPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/lowcoderApiPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/mongoPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/mssqlPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/mysqlPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/oraclePlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/postgresPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/redisPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/restApiPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/smtpPlugin/plugin.properties delete mode 100644 server/api-service/lowcoder-plugins/snowflakePlugin/plugin.properties create mode 100644 server/api-service/lowcoder-server/cert/README create mode 100644 server/api-service/lowcoder-server/cert/signing.p12 create mode 100644 server/api-service/lowcoder-server/src/main/assembly/assembly.xml create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/CustomWebFluxConfigurationSupport.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/APIDelayFilter.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextFilter.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/filter/ReactiveRequestContextHolder.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/LowcoderPluginManager.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PathBasedPluginLoader.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/ApiCallEventPublisher.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/RandomPasswordGeneratorConfig.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUser.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/AddSuperAdminUserImpl.java 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/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..92fb50ad9 --- /dev/null +++ b/server/api-service/PLUGIN.md @@ -0,0 +1,122 @@ +# Lowcoder plugin system (WIP) + +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 **LowcoderEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format: + +```java + @EndpointExtension(uri = , method = ) + public Mono (ServerRequest request) + { + ... your endpoint logic implementation + } + + for example: + + @EndpointExtension(uri = "/hello-world", method = Method.GET) + public Mono helloWorld(ServerRequest request) + { + return ServerResponse.ok().body(Mono.just(Hello.builder().message("Hello world!").build()), Hello.class); + } +``` +- TODO: class(es) impelemting **LowcoderDatasource** interface + +### LowcoderPlugin implementations + +Methods of interest: +- **pluginId()** - unique plugin ID - if a plugin with such ID is already loaded, subsequent plugins whith this ID will be ignored +- **description()** - short plugin description +- **load(ApplicationContext parentContext)** - is called during plugin startup - this is the place where you should completely initialize your plugin. If initialization fails, return false +- **unload()** - is called during lowcoder API server shutdown - this is the place where you should release all resources +- **endpoints()** - needs to contain all initialized **PluginEndpoints** you want to expose, for example: + +```java + @Override + public List endpoints() + { + List endpoints = new ArrayList<>(); + + endpoints.add(new HelloWorldEndpoint()); + + return endpoints; + } +``` +- **pluginInfo()** - should return a record object with additional information about your plugin. It is serialized to JSON as part of the **/plugins** listing (see **"info"** object in this example): + +```json +[ + { + "id": "example-plugin", + "description": "Example plugin for lowcoder platform", + "info": {} + }, + { + "id": "enterprise", + "description": "Lowcoder enterprise plugin", + "info": { + "enabledFeatures": [ + "endpointApiUsage" + ] + } + } +] +``` + +## TODOs + +1. Implement endpoint security - currently all plugin endpoints are public (probably by adding **security** attribute to **@EndpointExtension** and enforcing it) + + +## QUESTIONS / CONSIDERATIONS + +1. currently the plugin endpoints are prefixed with **/plugin/{pluginId}/** - this is hardcoded, do we want to make it configurable? + + 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..53ffadf95 --- /dev/null +++ b/server/api-service/lowcoder-dependencies/pom.xml @@ -0,0 +1,224 @@ + + + + + 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 + + + + 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 + + + + 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 + + + + + + + diff --git a/server/api-service/lowcoder-domain/pom.xml b/server/api-service/lowcoder-domain/pom.xml index 2150c484a..d7b96e027 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 @@ -186,6 +186,12 @@ es.moki.ratelimitj ratelimitj-redis + + + io.lettuce + lettuce-core + + @@ -242,6 +248,18 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + -parameters + + + + com.mysema.maven apt-maven-plugin @@ -268,9 +286,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/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/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/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/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..9b9da9549 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 @@ -86,7 +86,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 +96,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 +107,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,7 +145,7 @@ 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())) { @@ -155,19 +155,19 @@ public Mono create(Organization organization, String creatorId) { 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/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index e7526be8d..16ac4f8e2 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 @@ -335,7 +335,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/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/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/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..9918359bc 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 @@ -15,6 +15,8 @@ UTF-8 + UTF-8 + 17 @@ -171,4 +173,17 @@ validation-api + + + + + 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..8334e5562 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,6 +44,8 @@ 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(); public boolean isSelfHost() { @@ -158,4 +160,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-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 0000000000000000000000000000000000000000..2f336a1f6d694ca3ca35e2449cf903933bfd36fd GIT binary patch literal 4434 zcma)=XEYp)v&QXWSfTSRLBY;o}bym;h| z5^o=tF=15?j3f3oIJ9!{5a6dqZI$1=v}%W*pS0Fzd|e&CF%j@f<2ig4{1dNPhH5jP zTPBI_Oof?Y`uinLC;_hRP0`vTizS(%Lkplz_(zPyCR?}F#HLh^MR%HIBEb6M@ zeL=|QrKgX&>yx=i0Bo*4AzfN{6cp6rQiak(%cP`*MXytWQ!f1hU&Jm#K=8<8n#ylH}hx z+O3`0rMxINsd!SU^yD=+QLXN5#?pMoQ%-LVymO&_AI>>MSz)3w4KyiYqNVs1wK20t zfAM}F6FaTki9lVA$j%oX@hRa@QYt`O%moy+mkAJzzE2wvHZ0U-)Glg7PPtv*`$R6d zBy~jO9~w>_k-3hdEu$tnTZtK6lIgYn(sWu+Z^zl!pSk-XU2E#v@vyCWMi!0cfV+Lk z{iHARRG)?C@Fl#w|EiA^`FK9|Buuw6yl+jDIH5K|eST7kc~sHk79{9k0~dPL3Dy@( zT29kJscNVBhSg|4T88cJpg@3%LX=(TW0U-^o1X{BCVhybWyN6SiEy01aLaA*eqvj; z1cH_`7b_XAtp)*E(>-Zc@iD2DSd78-+J05LY~Or&z{PBK0Qh%T60W~cO>#)K%MP9id2(d3sp-H6Fk5@@Zh%z;Ko{j^Ca$1ES1QR?IM&Jl9p9(}2z z)*u-(@3{zEvkoEL%Se%^0R+kI*}#DG}>C;x2z-9f2hAGjMq(UQv@E!p}Vt5YpL` zPiwlf{fD~93zgb-BsThp`L=`)sIt4*F5`+0V}02p*Cv={BWr2DXnNv80+}H1=Yxm% zapV1Pdgk0`=^&`ZlTH{l_;J=YP`GVH!rb%5W1{?RtF<;D ziq&64=HFv2wrCQ1uds2A<0BMzwR10S8WCalxYw@1kLta6XdWxAskO;<-2!Eq_}+(_*36uj&=Jg3~aT&sZCvSZKAwHL%a2Qp!J{ zDVW4ONL_gBGH`p`-MsIE*d0?{!{5f- z+H1p_%yCTx6CF9>nW77SsF6yesTFJ_4M*+*tqC-hst7crmdQ zZxnc~(4cxl5;p>u_mn*=I%HoNnOxzBrit&M@G#cw#0k3r{c_n)*el7p2M}@W_j?qt zVy%Aj>&gx0tMRcs&L*?W)KS2Ygs46GnRA}40_9k#pim1)Z9;(pkfo9|9vlXG#*bx+~|eY$+>+nh62nhmg)7V(#9oUSge|dN8{U4tV zuo&jB)TrqWs#`Qh*qxJUJfNibdeGa}26!%RQn*k;u3LvJ7<^F8^--ZP6O5czA9c)^ zEI#4*W&S#NZ6JLO{xiUJU?$v;>|O`nk$uv32w_t}NhrnIdB$CnbF(sG?#3?1cjP=y!v|79bV0CL;N;ySZnPT37M$v3*$%kasOV%c*XZ27 zFz`jm6lx=2=xI_GPSOHu@)HHI z$1{i6j79{Gs&XM$Tu41^&{!`0BnRSt*__`oYG~|m%$Gqeo*5ygiV(Bg=O4OAG2yH) zKiv#>SzNy2Un4#W+5n49(#=IGKNUJGI88E7EW6y^*|=D;@R5+p+sx}2F8ct)tF_s8 z+xo#>U}*1VZ6qSWx~=hru}meg6g7 z6gf|{jjsXgMSsy3l7d10v1?#zAwp&ljt!12jvtKmUknkV#-r4CbhU*FiNjzL!Xhvc zNl76I7y?xPPZI$MjQ|z>jq-s2oWE1nzY5_0GKgfve=ru~X11jdi9R4EGgUTzm~Z`m z3}VH+`a-AIe5=gO?TR&wnIQ}Tf|5FqW>Y|RW=*>Tro9v7pv&1$C@=b|nTO;wo8a?H z>XSi3IVS?lXVY3GOM9hUU34YQ205W(=}6Twx1?;w1K0%1%hDHNUmw5ET754jbZ30T z0gY9>7pQufuVLW{`NIMyT%pvgQ-^#^olI{KQ4!BG$%e!*E4kyw1~+@NKgBb0R+|bd zOwe1T8lxGhdtHo)mRY({?&VE|W`L{6m9pL$sMIk!x}^+Dd^HEU*GU!*-E3O9%`|2=?rgDaQMG8qjXp~{RR z0W7>o^E5N$Pl;m)xl*KMnd8qo_9>Z(CnvGa0>>|Y|u)Z4^4CyVgGjx@{ z6}gjoVLx294W7--z^m_^_CJYMj|tXx7*q5X@xYSt5Pv{MhfV5~_5+ocXIoQwBZ{(n zXKB4I{82$D!&h;!Ss&TNvMT#ehE^IDP%BYws%LynRViHJHSEcuf;t00r!H1iRn92n z)Fo8d$<##vTxVKbAAd&r;I*5hq)VU*iz_POT}l9p*v;0T0j8s(q|*yN9??kOA47i2 zRQ@{$(S2}8O(-$7Rg!%b03MV4!+Ykd9UwQhWcp_xZYiti-R&k|e@~8?o$jzlHVoxH zcUd#_SgLt{gxbTR{dU5lRX#}v+X&YvdZZn^7cvzS{cbmGvCsETwL|0HuP2s6w)`SR z-sR*S0(vV{_sqA(iUYXm9*uh5UdyQ~ta@r6UK-KX$=+9Nfq%?RI63l;P@Xp`a4F$} z>^JRaHrGl4;wK-JW%>V@(Kt}8+DJ!BSC4(QNm68-HJn(uw$Q&->NmqODDY|f7=BAi z$o_OuZ-Y>#pQE`o!Y}U{e4m-@yHURrSkj30c~9y4Icn^HFym|9HPMSBy8^vzbsmo~ zD@QeISLwUtf@)Y-rCRfE|H`}ra{lPZ1!G^K2THD>o$y6z*1;1N;bfmaKV7pIflN+e z3f4uv$b{-I+5JWas;tob!N!OnV?zU;{t!vPmR;-pf`MTdg?*~d##oO7eF~wx<;Gr% zaGo>xY%0DjHyCYJE9zhKS-sU?-B&M;w)3n{kyB7)auuJJ{1Rdo99 z06Ermrr;t&7qR$D}`y~xDd6Dc{00E=^*35rXp%uT#=BnWVVKbt^zT~kvu zbk2{mmjllPEOXp8#Nl1@3eP-5wh2;^+xat2*;;lP8gF=A4vi<-p>qN$x!XG|PN;8_ zD|!!po#|IUTOmI09>px;dkk53(}vwVw71}WL2{}PM!sYUO}2zYlBu3Qz|ygAtOsuw z2ei|Cy(0(gbf;5wJMiic)hRxqAwLWs-mib2so`Fj^*S}EM`Vdnkai&Kb?s^o)3&_!^Ve75#DT{Pl!kWVVORifG)F#zYdx1^J?Yj(WV~GdHEG z^8H4JOz;g!3PuOX{hl#FhK$!7wT-V;=~>SiU`ZN=Lpe*>43%He2F5>Wbed&&ph1#2 z8L30(xicITv2*8xTPLD&C_F!8ho-QkriY^aU^*}%7|B0>1|R?*2M>z;&K#*1-R3uD zeyX)TITNIM`I;jW;TT|!14Hj>RafFsOZ6Qj;$mdy@{->(3F^WN4(2uY|La8k2NbVW Ag#Z8m literal 0 HcmV?d00001 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..61f9a79ca 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; @@ -93,12 +94,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); } 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..1cbfeef9a 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; 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..a5d9df955 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/configuration/PluginConfiguration.java @@ -0,0 +1,58 @@ +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; +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); + } + + @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..e0be8dac2 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java @@ -0,0 +1,104 @@ +package org.lowcoder.api.framework.plugin; + +import java.io.IOException; +import java.io.InputStream; +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(); + + 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 (name.startsWith("org.lowcoder.plugin.api.")) + { + 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, "org/lowcoder/plugin/api/", "org.lowcoder.plugin.api.")) + { + return appClassLoader.getResource(name); + } + return super.getResource(name); + } + + + @Override + public Enumeration getResources(String name) throws IOException + { + Objects.requireNonNull(name); + if (StringUtils.startsWithAny(name, "org/lowcoder/plugin/api/", "org.lowcoder.plugin.api.")) + { + 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..bcee69580 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java @@ -0,0 +1,195 @@ +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.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.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.stereotype.Component; +import org.springframework.web.reactive.function.server.HandlerFunction; +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)) + { + if (checkHandlerMethod(handler)) + { + + EndpointExtension endpointMeta = handler.getAnnotation(EndpointExtension.class); + String endpointName = endpoint.getClass().getSimpleName() + "_" + handler.getName(); + + RouterFunction routerFunction = route(createRequestPredicate(urlPrefix, endpointMeta), req -> + { + Mono result = null; + try + { + EndpointResponse response = (EndpointResponse)handler.invoke(endpoint, PluginServerRequest.fromServerRequest(req)); + result = createServerResponse(response); + } + catch (IllegalAccessException | InvocationTargetException cause) + { + throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !"); + } + return result; + }); + routes.add(routerFunction); + registerRouterFunctionMapping(endpointName, routerFunction); + + log.info("Registered endpoint: {} -> {}: {}", endpoint.getClass().getSimpleName(), endpointMeta.method(), urlPrefix + endpointMeta.uri()); + } + else + { + log.error("Cannot register plugin endpoint: {} -> {}! Handler method must be defined as: public Mono {}(ServerRequest request)", endpoint.getClass().getSimpleName(), handler.getName(), handler.getName()); + } + } + } + + private void registerRouterFunctionMapping(String endpointName, RouterFunction routerFunction) + { + String beanName = "pluginEndpoint_" + endpointName + "_" + System.currentTimeMillis(); + + ((GenericApplicationContext)applicationContext).registerBean(beanName, RouterFunction.class, () -> { + return 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/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..e1849c444 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java @@ -0,0 +1,94 @@ +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.core.annotation.AnnotationUtils; +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(" invocation :: {}", invocation.getMethod()); + + Method method = invocation.getMethod(); + EndpointExtension endpointExtension = AnnotationUtils.findAnnotation(method, EndpointExtension.class); + 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/security/SecurityConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java index b933a63e1..555c0a64b 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 @@ -146,7 +140,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 +219,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 d5fc1a4eb..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 @@ -246,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); @@ -340,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) 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/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/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/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/UserApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/UserApiService.java index 42161bd5a..252a4f837 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) 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..d7ad21a53 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 @@ -10,7 +10,14 @@ spring: allow-bean-definition-overriding: true allow-circular-references: true +logging: + level: + root: info + web: debug + server: + error: + includeStacktrace: ALWAYS compression: enabled: true forward-headers-strategy: NATIVE @@ -44,6 +51,11 @@ 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 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..30cd78b3b 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,7 +17,7 @@ spring: codec: max-in-memory-size: 20MB webflux: - context-path: / + base-path: / server: compression: @@ -53,6 +53,8 @@ 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} diff --git a/server/api-service/pom.xml b/server/api-service/pom.xml index 23ffce7ad..a04ba2dd2 100644 --- a/server/api-service/pom.xml +++ b/server/api-service/pom.xml @@ -1,335 +1,156 @@ - - - - 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.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 + + + + + + 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 + From da0c2aa4d07d92ae9a9b1aee1a141d14cf1dd7d3 Mon Sep 17 00:00:00 2001 From: Ludo Mikula Date: Tue, 5 Mar 2024 23:44:16 +0100 Subject: [PATCH 10/11] new: plugin endpoint security basics --- server/api-service/PLUGIN.md | 65 +---------------- server/api-service/lowcoder-sdk/pom.xml | 19 ++--- .../application/ApplicationController.java | 5 +- .../framework/plugin/PluginClassLoader.java | 12 ++-- .../endpoint/PluginEndpointHandlerImpl.java | 71 ++++++++++--------- .../EndpointAuthorizationManager.java | 24 +++++++ .../security/PluginAuthorizationManager.java | 8 +-- .../plugin/security/SecuredEndpoint.java | 16 +++++ server/api-service/pom.xml | 7 +- 9 files changed, 105 insertions(+), 122 deletions(-) create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java create mode 100644 server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java diff --git a/server/api-service/PLUGIN.md b/server/api-service/PLUGIN.md index 92fb50ad9..65a99adef 100644 --- a/server/api-service/PLUGIN.md +++ b/server/api-service/PLUGIN.md @@ -1,4 +1,4 @@ -# Lowcoder plugin system (WIP) +# Lowcoder backend plugin system This is an ongoing effort to refactor current plugin system based on pf4j library. @@ -50,73 +50,14 @@ Plugin jar can be structured in any way you like. It can be a plain java project It is composed from several parts: - class(es) implementing **LowcoderPlugin** interface -- class(es) implementing **LowcoderEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format: +- class(es) implementing **PluginEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format: ```java @EndpointExtension(uri = , method = ) - public Mono (ServerRequest request) + public EndpointResponse (EndpointRequest request) { ... your endpoint logic implementation } - - for example: - - @EndpointExtension(uri = "/hello-world", method = Method.GET) - public Mono helloWorld(ServerRequest request) - { - return ServerResponse.ok().body(Mono.just(Hello.builder().message("Hello world!").build()), Hello.class); - } ``` - TODO: class(es) impelemting **LowcoderDatasource** interface -### LowcoderPlugin implementations - -Methods of interest: -- **pluginId()** - unique plugin ID - if a plugin with such ID is already loaded, subsequent plugins whith this ID will be ignored -- **description()** - short plugin description -- **load(ApplicationContext parentContext)** - is called during plugin startup - this is the place where you should completely initialize your plugin. If initialization fails, return false -- **unload()** - is called during lowcoder API server shutdown - this is the place where you should release all resources -- **endpoints()** - needs to contain all initialized **PluginEndpoints** you want to expose, for example: - -```java - @Override - public List endpoints() - { - List endpoints = new ArrayList<>(); - - endpoints.add(new HelloWorldEndpoint()); - - return endpoints; - } -``` -- **pluginInfo()** - should return a record object with additional information about your plugin. It is serialized to JSON as part of the **/plugins** listing (see **"info"** object in this example): - -```json -[ - { - "id": "example-plugin", - "description": "Example plugin for lowcoder platform", - "info": {} - }, - { - "id": "enterprise", - "description": "Lowcoder enterprise plugin", - "info": { - "enabledFeatures": [ - "endpointApiUsage" - ] - } - } -] -``` - -## TODOs - -1. Implement endpoint security - currently all plugin endpoints are public (probably by adding **security** attribute to **@EndpointExtension** and enforcing it) - - -## QUESTIONS / CONSIDERATIONS - -1. currently the plugin endpoints are prefixed with **/plugin/{pluginId}/** - this is hardcoded, do we want to make it configurable? - - diff --git a/server/api-service/lowcoder-sdk/pom.xml b/server/api-service/lowcoder-sdk/pom.xml index 9918359bc..22e6cb815 100644 --- a/server/api-service/lowcoder-sdk/pom.xml +++ b/server/api-service/lowcoder-sdk/pom.xml @@ -13,13 +13,6 @@ lowcoder-sdk - - UTF-8 - UTF-8 - - 17 - - org.springframework.boot @@ -173,7 +166,17 @@ validation-api - + + + UTF-8 + UTF-8 + + 17 + + 17 + 17 + + 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 61f9a79ca..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 @@ -27,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; @@ -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/framework/plugin/PluginClassLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java index e0be8dac2..34945cdaf 100644 --- 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 @@ -1,7 +1,6 @@ package org.lowcoder.api.framework.plugin; import java.io.IOException; -import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; @@ -20,6 +19,11 @@ 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); @@ -34,7 +38,7 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE return clazz; } - if (name.startsWith("org.lowcoder.plugin.api.")) + if (StringUtils.startsWithAny(name, excludedPaths)) { try { @@ -67,7 +71,7 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE @Override public URL getResource(String name) { Objects.requireNonNull(name); - if (StringUtils.startsWithAny(name, "org/lowcoder/plugin/api/", "org.lowcoder.plugin.api.")) + if (StringUtils.startsWithAny(name, excludedPaths)) { return appClassLoader.getResource(name); } @@ -79,7 +83,7 @@ public URL getResource(String name) { public Enumeration getResources(String name) throws IOException { Objects.requireNonNull(name); - if (StringUtils.startsWithAny(name, "org/lowcoder/plugin/api/", "org.lowcoder.plugin.api.")) + if (StringUtils.startsWithAny(name, excludedPaths)) { return appClassLoader.getResources(name); } 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 index bcee69580..214252827 100644 --- 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 @@ -16,19 +16,23 @@ 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.HandlerFunction; import org.springframework.web.reactive.function.server.RequestPredicate; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerRequest; @@ -80,48 +84,47 @@ public List> registeredEndpoints() private void registerEndpointHandler(String urlPrefix, PluginEndpoint endpoint, Method handler) { - if (handler.isAnnotationPresent(EndpointExtension.class)) + if (!handler.isAnnotationPresent(EndpointExtension.class) || !checkHandlerMethod(handler)) { - if (checkHandlerMethod(handler)) + if (handler.isAnnotationPresent(EndpointExtension.class)) { - - EndpointExtension endpointMeta = handler.getAnnotation(EndpointExtension.class); - String endpointName = endpoint.getClass().getSimpleName() + "_" + handler.getName(); - - RouterFunction routerFunction = route(createRequestPredicate(urlPrefix, endpointMeta), req -> - { - Mono result = null; - try - { - EndpointResponse response = (EndpointResponse)handler.invoke(endpoint, PluginServerRequest.fromServerRequest(req)); - result = createServerResponse(response); - } - catch (IllegalAccessException | InvocationTargetException cause) - { - throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !"); - } - return result; - }); - routes.add(routerFunction); - registerRouterFunctionMapping(endpointName, routerFunction); - - log.info("Registered endpoint: {} -> {}: {}", endpoint.getClass().getSimpleName(), endpointMeta.method(), urlPrefix + endpointMeta.uri()); - } - else - { - log.error("Cannot register plugin endpoint: {} -> {}! Handler method must be defined as: public Mono {}(ServerRequest request)", endpoint.getClass().getSimpleName(), handler.getName(), handler.getName()); + 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, () -> { - return routerFunction; - }); - + ((GenericApplicationContext)applicationContext).registerBean(beanName, RouterFunction.class, () -> routerFunction ); log.debug("Registering RouterFunction bean definition: {}", beanName); } 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 index e1849c444..237567643 100644 --- 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 @@ -5,7 +5,6 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.lang3.StringUtils; import org.lowcoder.plugin.api.EndpointExtension; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; @@ -21,7 +20,7 @@ import reactor.core.publisher.Mono; @Slf4j -@Component +//@Component public class PluginAuthorizationManager implements ReactiveAuthorizationManager { private final MethodSecurityExpressionHandler expressionHandler; @@ -34,10 +33,9 @@ public PluginAuthorizationManager() @Override public Mono check(Mono authentication, MethodInvocation invocation) { - log.info(" invocation :: {}", invocation.getMethod()); + log.info("Checking plugin reactive endpoint invocation security for {}", invocation.getMethod().getName()); - Method method = invocation.getMethod(); - EndpointExtension endpointExtension = AnnotationUtils.findAnnotation(method, EndpointExtension.class); + EndpointExtension endpointExtension = (EndpointExtension)invocation.getArguments()[1]; if (endpointExtension == null || StringUtils.isBlank(endpointExtension.authorize())) { return Mono.empty(); 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/pom.xml b/server/api-service/pom.xml index a04ba2dd2..8ec6f774d 100644 --- a/server/api-service/pom.xml +++ b/server/api-service/pom.xml @@ -12,17 +12,12 @@ - 2.3.0-SNAPSHOT + 2.4.0 17 true true true - org.lowcoder - 1.0-SNAPSHOT true - 2.17.0 - 17 - 17 From 9f0e484f6d5a557ef3d459e73f0c972e1ee109d4 Mon Sep 17 00:00:00 2001 From: liusijun Date: Mon, 11 Mar 2024 16:58:35 +0800 Subject: [PATCH 11/11] fix: complete the locale file --- client/packages/lowcoder/src/i18n/locales/zh.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index d2e868a61..78f0fc071 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -140,7 +140,7 @@ prop: { expand: "展开", columns: "列", rowSelection: "行选择", - toolbar: "工具栏", + toolbar: "工具栏", pagination: "分页", logo: "标志", style: "样式", @@ -358,6 +358,7 @@ style: { textSize: "字体大小", textWeight: "字体粗细", "fontFamily": "字体", + "fontStyle": "字体风格", "backgroundImage": "背景图片", "backgroundImageRepeat" : "背景图片重复", "backgroundImageSize" : "背景图片大小", @@ -1086,7 +1087,7 @@ selectInput: { valueDesc: "当前选择的值", selectedIndexDesc: "当前选择的值的索引,如果未选择任何值则为-1", selectedLabelDesc: "当前选择的值的标签", -}, +}, file: { typeErrorMsg: "必须是一个带有有效文件大小单位的数字,或者是一个无单位的字节数.", fileEmptyErrorMsg: "上传失败.文件大小为空.", @@ -2327,7 +2328,7 @@ componentDoc: { event: "事件", eventName: "事件名称", eventDesc: "描述", - mehtod: "方法", + mehtod: "方法", methodUsage: "您可以通过方法与组件进行交互,并且可以在任何可以编写 JavaScript 的地方通过它们的名称调用它们.或者您可以通过事件的“控制组件”操作来调用它们.", methodName: "方法名称", methodDesc: "描述", @@ -2561,7 +2562,7 @@ componentDocExtra: { table: table, }, idSource: { - title: "用户认证提供商", + title: "用户认证提供商", form: "电子邮件", pay: "高级", enable: "启用", @@ -2723,4 +2724,4 @@ timeLine: { navStyle: "菜单风格", navItemStyle: "菜单项样式", } -}; \ No newline at end of file +};