diff --git a/.gitignore b/.gitignore index 2e1b56cd5..81c0e6c82 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ client/node_modules/ client/packages/lowcoder-plugin-demo/.yarn/install-state.gz client/packages/lowcoder-plugin-demo/yarn.lock client/packages/lowcoder-plugin-demo/.yarn/cache/@types-node-npm-16.18.68-56f72825c0-094ae9ed80.zip +application-dev.yml diff --git a/client/packages/lowcoder-comps/package.json b/client/packages/lowcoder-comps/package.json index 01733833e..029be11e2 100644 --- a/client/packages/lowcoder-comps/package.json +++ b/client/packages/lowcoder-comps/package.json @@ -1,6 +1,6 @@ { "name": "lowcoder-comps", - "version": "0.0.26", + "version": "0.0.27", "type": "module", "license": "MIT", "dependencies": { diff --git a/client/packages/lowcoder/src/components/CompName.tsx b/client/packages/lowcoder/src/components/CompName.tsx index d6a92dc46..47d9edea2 100644 --- a/client/packages/lowcoder/src/components/CompName.tsx +++ b/client/packages/lowcoder/src/components/CompName.tsx @@ -112,6 +112,13 @@ export const CompName = (props: Iprops) => { if (compInfo.isRemote) { + items.push({ + text: trans("history.currentVersion") + ": " + compInfo.packageVersion, + onClick: () => { + + }, + }); + items.push({ text: trans("comp.menuUpgradeToLatest"), onClick: () => { diff --git a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx index cb5a7836e..58dcfe8c4 100644 --- a/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/numberInputComp/numberInputComp.tsx @@ -52,6 +52,8 @@ import { import { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; const getStyle = (style: InputLikeStyleType) => { return css` @@ -372,7 +374,7 @@ const CustomInputNumber = (props: RecordConstructorToView) = ); }; -const NumberInputTmpComp = (function () { +let NumberInputTmpComp = (function () { return new UICompBuilder(childrenMap, (props) => { return props.label({ required: props.required, @@ -434,6 +436,8 @@ const NumberInputTmpComp = (function () { .build(); })(); +NumberInputTmpComp = migrateOldData(NumberInputTmpComp, fixOldInputCompData); + const NumberInputTmp2Comp = withMethodExposing( NumberInputTmpComp, refMethods([ diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx index f8b154e42..0cb4587a6 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/checkboxComp.tsx @@ -22,6 +22,8 @@ import { ValueFromOption } from "lowcoder-design"; import { EllipsisTextCss } from "lowcoder-design"; import { trans } from "i18n"; import { RefControl } from "comps/controls/refControl"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; export const getStyle = (style: CheckboxStyleType) => { return css` @@ -126,7 +128,7 @@ const CheckboxGroup = styled(AntdCheckboxGroup) <{ }} `; -const CheckboxBasicComp = (function () { +let CheckboxBasicComp = (function () { const childrenMap = { defaultValue: arrayStringExposingStateControl("defaultValue"), value: arrayStringExposingStateControl("value"), @@ -176,6 +178,8 @@ const CheckboxBasicComp = (function () { .build(); })(); +CheckboxBasicComp = migrateOldData(CheckboxBasicComp, fixOldInputCompData); + export const CheckboxComp = withExposingConfigs(CheckboxBasicComp, [ new NameConfig("value", trans("selectInput.valueDesc")), SelectInputInvalidConfig, diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx index a8c2c0dc1..c45c0cdc6 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/multiSelectComp.tsx @@ -14,9 +14,10 @@ import { SelectInputInvalidConfig, useSelectInputValidate } from "./selectInputC import { PaddingControl } from "../../controls/paddingControl"; import { MarginControl } from "../../controls/marginControl"; -import { useEffect, useRef } from "react"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; -const MultiSelectBasicComp = (function () { +let MultiSelectBasicComp = (function () { const childrenMap = { ...SelectChildrenMap, defaultValue: arrayStringExposingStateControl("defaultValue", ["1", "2"]), @@ -52,6 +53,8 @@ const MultiSelectBasicComp = (function () { .build(); })(); +MultiSelectBasicComp = migrateOldData(MultiSelectBasicComp, fixOldInputCompData); + export const MultiSelectComp = withExposingConfigs(MultiSelectBasicComp, [ new NameConfig("value", trans("selectInput.valueDesc")), new NameConfig("inputValue", trans("select.inputValueDesc")), diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx index 11bfceed0..4ab4add86 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/radioComp.tsx @@ -11,6 +11,8 @@ import { } from "./selectInputConstants"; import { EllipsisTextCss, ValueFromOption } from "lowcoder-design"; import { trans } from "i18n"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; +import { migrateOldData } from "comps/generators/simpleGenerators"; const getStyle = (style: RadioStyleType) => { return css` @@ -93,7 +95,7 @@ const Radio = styled(AntdRadioGroup)<{ }} `; -const RadioBasicComp = (function () { +let RadioBasicComp = (function () { return new UICompBuilder(RadioChildrenMap, (props) => { const [ validateState, @@ -129,6 +131,8 @@ const RadioBasicComp = (function () { .build(); })(); +RadioBasicComp = migrateOldData(RadioBasicComp, fixOldInputCompData); + export const RadioComp = withExposingConfigs(RadioBasicComp, [ new NameConfig("value", trans("selectInput.valueDesc")), SelectInputInvalidConfig, diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx index 73a7d4675..a73827c2a 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/segmentedControl.tsx @@ -25,6 +25,9 @@ import { RefControl } from "comps/controls/refControl"; import { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; + const getStyle = (style: SegmentStyleType) => { return css` @@ -83,7 +86,7 @@ const SegmentChildrenMap = { ...formDataChildren, }; -const SegmentedControlBasicComp = (function () { +let SegmentedControlBasicComp = (function () { return new UICompBuilder(SegmentChildrenMap, (props) => { const [ validateState, @@ -147,6 +150,8 @@ const SegmentedControlBasicComp = (function () { .build(); })(); +SegmentedControlBasicComp = migrateOldData(SegmentedControlBasicComp, fixOldInputCompData); + export const SegmentedControlComp = withExposingConfigs(SegmentedControlBasicComp, [ new NameConfig("value", trans("selectInput.valueDesc")), SelectInputInvalidConfig, diff --git a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx index 1a30f2522..50455b335 100644 --- a/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/selectInputComp/selectComp.tsx @@ -17,8 +17,10 @@ import { } from "./selectInputConstants"; import { useRef } from "react"; import { RecordConstructorToView } from "lowcoder-core"; +import { fixOldInputCompData } from "../textInputComp/textInputConstants"; +import { migrateOldData } from "comps/generators/simpleGenerators"; -const SelectBasicComp = (function () { +let SelectBasicComp = (function () { const childrenMap = { ...SelectChildrenMap, defaultValue: stringExposingStateControl("defaultValue"), @@ -55,6 +57,8 @@ const SelectBasicComp = (function () { .build(); })(); +SelectBasicComp = migrateOldData(SelectBasicComp, fixOldInputCompData); + export const SelectComp = withExposingConfigs(SelectBasicComp, [ new NameConfig("value", trans("selectInput.valueDesc")), new NameConfig("inputValue", trans("select.inputValueDesc")), diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx index fc34bc723..5eacf07cf 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/inputComp.tsx @@ -11,6 +11,7 @@ import styled from "styled-components"; import { UICompBuilder } from "../../generators"; import { FormDataPropertyView } from "../formComp/formDataConstants"; import { + fixOldInputCompData, getStyle, inputRefMethods, TextInputBasicSection, @@ -30,6 +31,7 @@ import { IconControl } from "comps/controls/iconControl"; import { hasIcon } from "comps/utils"; import { InputRef } from "antd/es/input"; import { RefControl } from "comps/controls/refControl"; +import { migrateOldData } from "comps/generators/simpleGenerators"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; @@ -52,7 +54,7 @@ const childrenMap = { suffixIcon: IconControl, }; -export const InputComp = new UICompBuilder(childrenMap, (props) => { +let InputBasicComp = new UICompBuilder(childrenMap, (props) => { const [inputProps, validateState] = useTextInputProps(props); return props.label({ required: props.required, @@ -108,3 +110,8 @@ export const InputComp = new UICompBuilder(childrenMap, (props) => { ...TextInputConfigs, ]) .build(); + + +const InputComp = migrateOldData(InputBasicComp, fixOldInputCompData); + +export { InputComp }; diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx index 51815260f..9bad13d1e 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/mentionComp.tsx @@ -12,6 +12,7 @@ import { UICompBuilder } from "../../generators"; import { FormDataPropertyView } from "../formComp/formDataConstants"; import { checkMentionListData, + fixOldInputCompData, textInputChildren, } from "./textInputConstants"; import { @@ -42,7 +43,7 @@ import { blurMethod, focusWithOptions } from "comps/utils/methodUtils"; import { textInputValidate, } from "../textInputComp/textInputConstants"; -import { jsonControl } from "@lowcoder-ee/comps/controls/codeControl"; +import { jsonControl } from "comps/controls/codeControl"; import { submitEvent, eventHandlerControl, @@ -54,6 +55,7 @@ import { import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; const Wrapper = styled.div<{ $style: InputLikeStyleType; @@ -267,12 +269,15 @@ let MentionTmpComp = (function () { .build(); })(); + MentionTmpComp = class extends MentionTmpComp { override autoHeight(): boolean { return this.children.autoHeight.getView(); } }; +MentionTmpComp = migrateOldData(MentionTmpComp, fixOldInputCompData); + const TextareaTmp2Comp = withMethodExposing( MentionTmpComp, refMethods([focusWithOptions, blurMethod]) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx index b5c3d701d..7659cdf72 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/passwordComp.tsx @@ -13,6 +13,7 @@ import { LabelControl } from "../../controls/labelControl"; import { UICompBuilder, withDefault } from "../../generators"; import { FormDataPropertyView } from "../formComp/formDataConstants"; import { + fixOldInputCompData, getStyle, inputRefMethods, TextInputBasicSection, @@ -40,6 +41,7 @@ import { hasIcon } from "comps/utils"; import { RefControl } from "comps/controls/refControl"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; const PasswordStyle = styled(InputPassword)<{ $style: InputLikeStyleType; @@ -47,7 +49,7 @@ const PasswordStyle = styled(InputPassword)<{ ${(props) => props.$style && getStyle(props.$style)} `; -const PasswordTmpComp = (function () { +let PasswordTmpComp = (function () { const childrenMap = { ...textInputChildren, viewRef: RefControl, @@ -111,6 +113,8 @@ const PasswordTmpComp = (function () { .build(); })(); +PasswordTmpComp = migrateOldData(PasswordTmpComp, fixOldInputCompData); + const PasswordTmp2Comp = withMethodExposing(PasswordTmpComp, inputRefMethods); export const PasswordComp = withExposingConfigs(PasswordTmp2Comp, [ diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx index e11027a69..fe6a4ad24 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textAreaComp.tsx @@ -10,6 +10,7 @@ import { AutoHeightControl } from "../../controls/autoHeightControl"; import { UICompBuilder, withDefault } from "../../generators"; import { FormDataPropertyView } from "../formComp/formDataConstants"; import { + fixOldInputCompData, getStyle, TextInputBasicSection, textInputChildren, @@ -35,6 +36,7 @@ import { blurMethod, focusWithOptions } from "comps/utils/methodUtils"; import React, { useContext } from "react"; import { EditorContext } from "comps/editorState"; +import { migrateOldData } from "comps/generators/simpleGenerators"; const TextAreaStyled = styled(TextArea)<{ $style: InputLikeStyleType; @@ -126,6 +128,8 @@ TextAreaTmpComp = class extends TextAreaTmpComp { } }; +TextAreaTmpComp = migrateOldData(TextAreaTmpComp, fixOldInputCompData); + const TextareaTmp2Comp = withMethodExposing( TextAreaTmpComp, refMethods([focusWithOptions, blurMethod]) diff --git a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx index 1d01266af..9c9d17cbb 100644 --- a/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx +++ b/client/packages/lowcoder/src/comps/comps/textInputComp/textInputConstants.tsx @@ -305,3 +305,17 @@ export function checkMentionListData(data: any) { } return data } + +// separate defaultValue and value for old components +export function fixOldInputCompData(oldData: any) { + if (!oldData) return oldData; + if (Boolean(oldData.value) && !Boolean(oldData.defaultValue)) { + const value = oldData.value; + return { + ...oldData, + defaultValue: value, + value: '', + }; + } + return oldData; +} diff --git a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginCompItem.tsx b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginCompItem.tsx index 43f211c7f..e6895fd2c 100644 --- a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginCompItem.tsx +++ b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/PluginCompItem.tsx @@ -96,7 +96,7 @@ export function PluginCompItem(props: PluginCompItemProps) {
{compMeta.name}
-
{compMeta.description || "No description."}
+
{compMeta.description || "No description."} v{packageVersion || ""}
); diff --git a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx index 427b586a8..61c0d9210 100644 --- a/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx +++ b/client/packages/lowcoder/src/pages/editor/right/PluginPanel/index.tsx @@ -7,7 +7,7 @@ import { getUser } from "redux/selectors/usersSelectors"; import { BluePlusIcon, CustomModal, DocLink, TacoButton, TacoInput } from "lowcoder-design"; import { getCommonSettings } from "redux/selectors/commonSettingSelectors"; import styled from "styled-components"; -import { normalizeNpmPackage, validateNpmPackage } from "comps/utils/remote"; +import { getNpmPackageMeta, normalizeNpmPackage, validateNpmPackage } from "comps/utils/remote"; import { ComListTitle, ExtensionContentWrapper } from "../styledComponent"; import { EmptyContent } from "components/EmptyContent"; import { messageInstance } from "lowcoder-design"; @@ -37,6 +37,8 @@ export default function PluginPanel() { [commonSettings?.npmPlugins] ); + console.log("plugins: ", plugins); + const handleSetNpmPlugins = (nextNpmPlugins: string[]) => { dispatch( setCommonSettings({ diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 6f55ed0fc..c536b8a24 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -2,20 +2,14 @@ ## Build Lowcoder api-service application ## FROM maven:3.9-eclipse-temurin-17 AS build-api-service + +# Build lowcoder-api COPY ./server/api-service /lowcoder-server WORKDIR /lowcoder-server RUN --mount=type=cache,target=/root/.m2 mvn -f pom.xml clean package -DskipTests # Create required folder structure -RUN mkdir -p /lowcoder/api-service/plugins /lowcoder/api-service/config /lowcoder/api-service/logs - -# Define lowcoder main jar and plugin jars -ARG JAR_FILE=/lowcoder-server/lowcoder-server/target/lowcoder-server-*.jar -ARG PLUGIN_JARS=/lowcoder-server/lowcoder-plugins/*/target/*.jar - -# Copy lowcoder server application and plugins -RUN cp ${JAR_FILE} /lowcoder/api-service/server.jar \ - && cp ${PLUGIN_JARS} /lowcoder/api-service/plugins/ +RUN mkdir -p /lowcoder/api-service/config /lowcoder/api-service/logs /lowcoder/plugins # Copy lowcoder server configuration COPY server/api-service/lowcoder-server/src/main/resources/selfhost/ce/application.yml /lowcoder/api-service/config/ @@ -43,6 +37,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends gosu \ # Copy lowcoder server configuration COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder/api-service /lowcoder/api-service +# Copy lowcoder api service app, dependencies and libs +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/app /lowcoder/api-service/app +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/dependencies /lowcoder/api-service/dependencies +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/libs /lowcoder/api-service/libs +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/plugins /lowcoder/api-service/plugins +COPY --chown=lowcoder:lowcoder --from=build-api-service /lowcoder-server/distribution/target/lowcoder-api-service-bin/set-classpath.sh /lowcoder/api-service/set-classpath.sh + EXPOSE 8080 CMD [ "sh" , "/lowcoder/api-service/entrypoint.sh" ] @@ -202,6 +203,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-instal # Add lowcoder api-service COPY --chown=lowcoder:lowcoder --from=lowcoder-ce-api-service /lowcoder/api-service /lowcoder/api-service +RUN mkdir -p /lowcoder/plugins/ && chown lowcoder:lowcoder /lowcoder/plugins/ # Add lowcoder node-service COPY --chown=lowcoder:lowcoder --from=lowcoder-ce-node-service /lowcoder/node-service /lowcoder/node-service diff --git a/deploy/docker/api-service/entrypoint.sh b/deploy/docker/api-service/entrypoint.sh index 5f2e3ad2e..0f43580fe 100644 --- a/deploy/docker/api-service/entrypoint.sh +++ b/deploy/docker/api-service/entrypoint.sh @@ -27,12 +27,16 @@ ${JAVA_HOME}/bin/java -version echo cd /lowcoder/api-service +source set-classpath.sh + exec gosu ${USER_ID}:${GROUP_ID} ${JAVA_HOME}/bin/java \ + -Djava.util.prefs.userRoot=/tmp \ -Djava.security.egd=file:/dev/./urandom \ -Dhttps.protocols=TLSv1.1,TLSv1.2 \ -Dlog4j2.formatMsgNoLookups=true \ -Dspring.config.location="file:///lowcoder/api-service/config/application.yml,file:///lowcoder/api-service/config/application-selfhost.yml" \ --add-opens java.base/java.nio=ALL-UNNAMED \ + -cp "${LOWCODER_CLASSPATH:=.}" \ ${JAVA_OPTS} \ - -jar "${APP_JAR}" --spring.webflux.base-path=${CONTEXT_PATH} ${CUSTOM_APP_PROPERTIES} + org.lowcoder.api.ServerApplication --spring.webflux.base-path=${CONTEXT_PATH} ${CUSTOM_APP_PROPERTIES} diff --git a/server/api-service/.gitignore b/server/api-service/.gitignore index 044c6298e..a9fc541a9 100644 --- a/server/api-service/.gitignore +++ b/server/api-service/.gitignore @@ -23,8 +23,9 @@ dependency-reduced-pom.xml .run/** logs/** tmp/** -/openblocks-server/logs/ +# Ignore plugin.properties which are generated dynamically +**/plugin.properties # to ignore the node_modeules folder node_modules @@ -34,5 +35,4 @@ package-lock.json # test coverage coverage-summary.json app/client/cypress/locators/Widgets.json -/openblocks-domain/logs/ -application-lowcoder.yml \ No newline at end of file +application-lowcoder.yml diff --git a/server/api-service/PLUGIN.md b/server/api-service/PLUGIN.md new file mode 100644 index 000000000..65a99adef --- /dev/null +++ b/server/api-service/PLUGIN.md @@ -0,0 +1,63 @@ +# Lowcoder backend plugin system + +This is an ongoing effort to refactor current plugin system based on pf4j library. + +## Reasoning + +1. create a cleaner and simpler plugin system with clearly defined purpose(s) (new endpoints, new datasource types, etc..) +2. lowcoder does not need live plugin loading/reloading/unloading/updates, therefore the main feature of pf4j is rendered useless, in fact it adds a lot of complexity due to classloaders used for managing plugins (especially in spring/boot applications) +3. simpler and easier plugin detection - just a jar with a class implementing a common interface (be it a simple pojo project or a complex spring/boot implementation) + +## How it works + +The main entrypoint for plugin system is in **lowcoder-server** module with class **org.lowcoder.api.framework.configuration.PluginConfiguration** +It creates: +- LowcoderPluginManager bean which is responsible for plugin lifecycle management +- Adds plugin defined endpoints to lowcoder by creating **pluginEndpoints** bean +- TODO: Adds plugin defined datasources to lowcoder by creating **pluginDatasources** bean + +### lowcoder-plugin-api library + +This library contains APIs for plugin implementations. +It is used by both, lowcoder API server as well as all plugins. + +### PluginLoader + +The sole purpose of a PluginLoader is to find plugin candidates and load them into VM. +There is currently one implementation that based on paths - **PathBasedPluginLoader**, it: +- looks in folders and subfolders defined in **application.yaml** - entries can point to a folder or specific jar file. If a relative path is supplied, the location of lowcoder API server application jar is used as parent folder (when run in non-packaged state, eg. in IDE, it uses the folder where ServerApplication.class is generated) + +```yaml +common: + plugin-dirs: + - plugins + - /some/custom/path/myGreatPlugin.jar +``` +- finds all **jar**(s) and inspects them for classes implementing **LowcoderPlugin** interface +- instantiates all LowcoderPlugin implementations + +### LowcoderPluginManager + +The main job of plugin manager is to: +- register plugins found and instantiated by **PluginLoader** +- start registered plugins by calling **LowcoderPlugin.load()** method +- create and register **RouterFunction**(s) for all loaded plugin endpoints +- TODO: create and register datasources for all loaded plugin datasources + +## Plugin project structure + +Plugin jar can be structured in any way you like. It can be a plain java project, but also a spring/boot based project or based on any other framework. + +It is composed from several parts: +- class(es) implementing **LowcoderPlugin** interface +- class(es) implementing **PluginEndpoint** interface, containing endpoint handler functions marked with **@EndpointExtension** annotation. These functions must obey following format: + +```java + @EndpointExtension(uri = , method = ) + public EndpointResponse (EndpointRequest request) + { + ... your endpoint logic implementation + } +``` +- TODO: class(es) impelemting **LowcoderDatasource** interface + diff --git a/server/api-service/distribution/pom.xml b/server/api-service/distribution/pom.xml new file mode 100644 index 000000000..d68b3fab4 --- /dev/null +++ b/server/api-service/distribution/pom.xml @@ -0,0 +1,84 @@ + + 4.0.0 + + org.lowcoder + lowcoder-root + ${revision} + + + distribution + pom + + + ${project.build.directory}/dependencies + + + + + + + org.lowcoder + lowcoder-sdk + + + org.lowcoder + lowcoder-infra + + + org.lowcoder + lowcoder-domain + + + org.lowcoder + lowcoder-server + + + + + lowcoder-api-service + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${assembly.lib.directory} + false + false + true + true + + + + + + maven-assembly-plugin + + + distro-assembly + package + + single + + + false + + src/assembly/bin.xml + + + + + + + + + \ No newline at end of file diff --git a/server/api-service/distribution/src/assembly/bin.xml b/server/api-service/distribution/src/assembly/bin.xml new file mode 100644 index 000000000..b6422619e --- /dev/null +++ b/server/api-service/distribution/src/assembly/bin.xml @@ -0,0 +1,72 @@ + + bin + + dir + + false + + + + src/assembly/set-classpath.sh + + + + + + ${assembly.lib.directory} + dependencies + + ${project.groupId}:* + + + + + + + + true + + org.lowcoder:lowcoder-server + + + app + false + false + + + + + + true + + org.lowcoder:lowcoder-domain + org.lowcoder:lowcoder-infra + org.lowcoder:lowcoder-sdk + + + libs + false + false + + + + + + true + true + + org.lowcoder:*Plugin + + + org.lowcoder:sqlBasedPlugin + + + plugins + false + false + + + + \ No newline at end of file diff --git a/server/api-service/distribution/src/assembly/set-classpath.sh b/server/api-service/distribution/src/assembly/set-classpath.sh new file mode 100755 index 000000000..de82ddf7f --- /dev/null +++ b/server/api-service/distribution/src/assembly/set-classpath.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# +# Set lowcoder api service classpath for use in startup script +# +export LOWCODER_CLASSPATH="`find libs/ dependencies/ app/ -type f -name "*.jar" | tr '\n' ':' | sed -e 's/:$//'`" + +# +# Example usage: +# +# java -cp "${LOWCODER_CLASSPATH}" org.lowcoder.api.ServerApplication diff --git a/server/api-service/lowcoder-dependencies/pom.xml b/server/api-service/lowcoder-dependencies/pom.xml new file mode 100644 index 000000000..771a160c2 --- /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 + 20231013 + + + + 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/asset/service/AssetServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/asset/service/AssetServiceImpl.java index 405734f47..ff802c5d7 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/asset/service/AssetServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/asset/service/AssetServiceImpl.java @@ -61,7 +61,7 @@ public Mono upload(Part filePart, int maxFileSizeKB, boolean isThumbnail) // The reason we restrict file types here is to avoid having to deal with dangerous image types such as SVG, // which can have arbitrary HTML/JS inside of them. - + final MediaType contentType = filePart.headers().getContentType(); if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) { return Mono.error(new BizException(BizError.INVALID_PARAMETER, "INCORRECT_IMAGE_TYPE")); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java deleted file mode 100644 index 18d73fdf5..000000000 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/configurations/Pf4jConfiguration.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.lowcoder.domain.configurations; - -import org.pf4j.spring.SpringPluginManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class Pf4jConfiguration { - - @Bean - public SpringPluginManager pluginManager() { - return new SpringPluginManager(); - } - -} diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java index a254da0f1..88bc8b7da 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/folder/model/Folder.java @@ -17,4 +17,9 @@ public class Folder extends HasIdAndAuditing { @Nullable private String parentFolderId; // null represents folder in the root folder private String name; + private String title; + private String description; + private String category; + private String type; + private String image; } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/group/model/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..22e6cb815 100644 --- a/server/api-service/lowcoder-sdk/pom.xml +++ b/server/api-service/lowcoder-sdk/pom.xml @@ -5,7 +5,7 @@ lowcoder-root org.lowcoder - ${revision} + ${revision} 4.0.0 @@ -13,11 +13,6 @@ lowcoder-sdk - - UTF-8 - 17 - - org.springframework.boot @@ -171,4 +166,27 @@ validation-api + + + UTF-8 + UTF-8 + + 17 + + 17 + 17 + + + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + + diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java index d1fcf3ea8..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 000000000..2f336a1f6 Binary files /dev/null and b/server/api-service/lowcoder-server/cert/signing.p12 differ diff --git a/server/api-service/lowcoder-server/pom.xml b/server/api-service/lowcoder-server/pom.xml index cd2d2ed86..5021d2b61 100644 --- a/server/api-service/lowcoder-server/pom.xml +++ b/server/api-service/lowcoder-server/pom.xml @@ -1,281 +1,380 @@ - - 4.0.0 - - lowcoder-root - org.lowcoder - ${revision} - + + 4.0.0 + + lowcoder-root + org.lowcoder + ${revision} + - lowcoder-server - jar + lowcoder-server + jar - lowcoder-server + lowcoder-server - - 17 - false - ${skipTests} - ${skipTests} - + + UTF-8 + UTF-8 - + 17 - - org.lowcoder - lowcoder-sdk - - - org.lowcoder - lowcoder-infra - - - org.lowcoder - lowcoder-domain - + false + ${skipTests} + ${skipTests} - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.security - spring-security-config - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springdoc - springdoc-openapi-starter-webflux-ui - 2.2.0 - - - io.projectreactor.tools - blockhound - - - org.springframework.boot - spring-boot-starter-data-mongodb-reactive - + cert/signing.p12 + pkcs12 + dev + lowcoder + ${keystore.password} + ${keystore.password} + - - org.springframework.boot - spring-boot-starter-data-redis-reactive - + - - org.projectlombok - lombok - + + org.lowcoder + lowcoder-sdk + + + org.lowcoder + lowcoder-infra + + + org.lowcoder + lowcoder-domain + + + org.lowcoder.plugin + lowcoder-plugin-api + - com.google.guava - guava - - - commons-io - commons-io - - - org.springframework.boot - spring-boot-starter-actuator - - - io.micrometer - micrometer-registry-prometheus - - - io.sentry - sentry-spring-boot-starter - - - org.apache.httpcomponents - httpclient - - - org.apache.commons - commons-text - - - - org.apache.commons - commons-collections4 - - + + org.apache.commons + commons-collections4 + + + - - io.netty - netty-all - runtime - - - io.projectreactor - reactor-tools - - - org.mockito - mockito-inline - test - - - org.mockito - mockito-core - test - - - junit - junit - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - io.projectreactor - reactor-test - test - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo.spring30x - test - - - com.jayway.jsonpath - json-path - - - jakarta.servlet - jakarta.servlet-api - + + io.netty + netty-all + runtime + + + io.projectreactor + reactor-tools + + + org.mockito + mockito-inline + test + + + org.mockito + mockito-core + test + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + io.projectreactor + reactor-test + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo.spring30x + test + + + com.jayway.jsonpath + json-path + + + jakarta.servlet + jakarta.servlet-api + - - com.auth0 - java-jwt - 4.4.0 - + + com.auth0 + java-jwt + 4.4.0 + - - it.ozimov - embedded-redis - 0.7.3 - test - - - org.apache.directory.server - apacheds-test-framework - 2.0.0.AM26 - test - - - org.junit.vintage - junit-vintage-engine - 5.9.3 - test - - - io.jsonwebtoken - jjwt-api - 0.11.5 - compile - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - compile - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - + + org.passay + passay + 1.6.3 + + + + it.ozimov + embedded-redis + 0.7.3 + test + + + org.apache.directory.server + apacheds-test-framework + 2.0.0.AM26 + test + + + org.junit.vintage + junit-vintage-engine + 5.9.3 + test + + + io.jsonwebtoken + jjwt-api + 0.11.5 + compile + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + compile + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + org.springframework + spring-aspects + + + org.springframework + spring-aspects + + + + + + + + org.lowcoder + lowcoder-dependencies + ${revision} + pom + import + + + - + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + -parameters + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.lowcoder.api.ServerApplication + true + true + true + + + + + + org.apache.maven.plugins + maven-jarsigner-plugin + 3.0.0 + + + sign + + sign + + + + verify + + verify + + + + + ${keystore.type} + ${keystore.path} + ${keystore.alias} + ${keystore.store.password} + ${keystore.key.password} + + - - - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - 3.1.2 - - ${skipUnitTests} - - **/*IntegrationTest.java - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - ${skipIntegrationTests} - - **/*IntegrationTest.java - - - -Dpf4j.pluginsDir=../lowcoder-plugins/plugins - - - - - - integration-test - verify - - - - - - maven-antrun-plugin - - - copy-plugins-jar-for-integration-tests - pre-integration-test - - - - - - - - - - run - - - - delete-plugins-after-integration-tests-phase - post-integration-test - - - - - - - run - - - - - - + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + ${skipUnitTests} + + **/*IntegrationTest.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${skipIntegrationTests} + + **/*IntegrationTest.java + + + -Dpf4j.pluginsDir=../lowcoder-plugins/plugins + + + + + + integration-test + verify + + + + + + maven-antrun-plugin + + + copy-plugins-jar-for-integration-tests + pre-integration-test + + + + + + + + + + run + + + + delete-plugins-after-integration-tests-phase + post-integration-test + + + + + + + run + + + + + + diff --git a/server/api-service/lowcoder-server/src/main/assembly/assembly.xml b/server/api-service/lowcoder-server/src/main/assembly/assembly.xml new file mode 100644 index 000000000..b2f6bb420 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/assembly/assembly.xml @@ -0,0 +1,58 @@ + + + lowcoder-dist + + dir + + + true + lowcoder + + + + target/${project.artifactId}-${project.version}.jar + + application.jar + + + + + + + \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java index 3a442255b..09c94ee06 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/ServerApplication.java @@ -45,6 +45,9 @@ public void init() { public static void main(String[] args) { + /** Disable Java Flight Recorder for Redis Lettuce driver **/ + System.setProperty("io.lettuce.core.jfr", "false"); + Schedulers.enableMetrics(); new SpringApplicationBuilder(ServerApplication.class) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java index d12297b33..de398e01f 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java @@ -1,11 +1,12 @@ package org.lowcoder.api.application; import static org.apache.commons.collections4.SetUtils.emptyIfNull; -import static org.lowcoder.infra.event.EventType.APPLICATION_CREATE; -import static org.lowcoder.infra.event.EventType.APPLICATION_DELETE; -import static org.lowcoder.infra.event.EventType.APPLICATION_RECYCLED; -import static org.lowcoder.infra.event.EventType.APPLICATION_RESTORE; -import static org.lowcoder.infra.event.EventType.APPLICATION_UPDATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_CREATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_DELETE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_RECYCLED; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_RESTORE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_UPDATE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_VIEW; import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; @@ -26,7 +27,6 @@ import org.lowcoder.domain.application.model.ApplicationStatus; import org.lowcoder.domain.application.model.ApplicationType; import org.lowcoder.domain.permission.model.ResourceRole; -import org.lowcoder.infra.event.EventType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -93,12 +93,11 @@ public Mono> getEditingApplication(@PathVariable S .map(ResponseView::success); } - // will call the check in ApplicationApiService and ApplicationService @Override public Mono> getPublishedApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_ALL) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW)) .map(ResponseView::success); } @@ -106,7 +105,7 @@ public Mono> getPublishedApplication(@PathVariable public Mono> getPublishedMarketPlaceApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.PUBLIC_TO_MARKETPLACE) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW)) .map(ResponseView::success); } @@ -114,7 +113,7 @@ public Mono> getPublishedMarketPlaceApplication(@P public Mono> getAgencyProfileApplication(@PathVariable String applicationId) { return applicationApiService.getPublishedApplication(applicationId, ApplicationRequestType.AGENCY_PROFILE) .delayUntil(applicationView -> applicationApiService.updateUserApplicationLastViewTime(applicationId)) - .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, EventType.VIEW)) + .delayUntil(applicationView -> businessEventPublisher.publishApplicationCommonEvent(applicationView, APPLICATION_VIEW)) .map(ResponseView::success); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java index 166801e7d..c28740cdc 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java @@ -149,7 +149,7 @@ public Mono loginOrRegister(AuthUser authUser, ServerWebExchange exchange, boolean createWorkspace = authUser.getOrgId() == null && StringUtils.isBlank(invitationId) && authProperties.getWorkspaceCreation(); if (user.getIsNewUser() && createWorkspace) { - return onUserRegister(user); + return onUserRegister(user, false); } return Mono.empty(); }) @@ -166,7 +166,7 @@ public Mono loginOrRegister(AuthUser authUser, ServerWebExchange exchange, .then(businessEventPublisher.publishUserLoginEvent(authUser.getSource())); } - private Mono updateOrCreateUser(AuthUser authUser, boolean linkExistingUser) { + public Mono updateOrCreateUser(AuthUser authUser, boolean linkExistingUser) { if(linkExistingUser) { return sessionUserService.getVisitor() @@ -256,8 +256,8 @@ protected Connection getAuthConnection(AuthUser authUser, User user) { .get(); } - protected Mono onUserRegister(User user) { - return organizationService.createDefault(user).then(); + public Mono onUserRegister(User user, boolean isSuperAdmin) { + return organizationService.createDefault(user, isSuperAdmin).then(); } protected Mono onUserLogin(String orgId, User user, String source) { @@ -362,7 +362,7 @@ private Mono removeTokensByAuthId(String authId) { private Mono checkIfAdmin() { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.empty(); } return deferredError(BizError.NOT_AUTHORIZED, "NOT_AUTHORIZED"); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java index 1494f7786..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..34945cdaf --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginClassLoader.java @@ -0,0 +1,108 @@ +package org.lowcoder.api.framework.plugin; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; + +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +public class PluginClassLoader extends URLClassLoader +{ + private static final ClassLoader baseClassLoader = ClassLoader.getPlatformClassLoader(); + private final ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader(); + + private static final String[] excludedPaths = new String[] { + "org.lowcoder.plugin.api.", + "org/lowcoder/plugin/api/" + }; + + public PluginClassLoader(String name, Path pluginPath) + { + super(name, pathToURLs(pluginPath), baseClassLoader); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException + { + Class clazz = findLoadedClass(name); + if (clazz != null) + { + return clazz; + } + + if (StringUtils.startsWithAny(name, excludedPaths)) + { + try + { + clazz = appClassLoader.loadClass(name); + return clazz; + } + catch(Throwable cause) + { + log.error("[{}] :: Error loading class with appClassLoader - {}", name, cause.getMessage(), cause ); + } + } + + + try + { + clazz = super.loadClass(name, resolve); + if (clazz != null) + { + return clazz; + } + } + catch(NoClassDefFoundError cause) + { + log.error("[{}] :: Error loading class - {}", name, cause.getMessage(), cause ); + } + + return null; + } + + @Override + public URL getResource(String name) { + Objects.requireNonNull(name); + if (StringUtils.startsWithAny(name, excludedPaths)) + { + return appClassLoader.getResource(name); + } + return super.getResource(name); + } + + + @Override + public Enumeration getResources(String name) throws IOException + { + Objects.requireNonNull(name); + if (StringUtils.startsWithAny(name, excludedPaths)) + { + return appClassLoader.getResources(name); + } + return super.getResources(name); + } + + private static URL[] pathToURLs(Path path) + { + URL[] urls = null; + try + { + urls = new URL[] { path.toUri().toURL() }; + } + catch(MalformedURLException cause) + { + /** should not happen **/ + } + + return urls; + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java new file mode 100644 index 000000000..bbce19994 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginExecutor.java @@ -0,0 +1,36 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.Map; + +import org.lowcoder.plugin.api.LowcoderPlugin; +import org.lowcoder.plugin.api.LowcoderServices; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PluginExecutor extends Thread +{ + private Map env; + private LowcoderPlugin plugin; + private LowcoderServices services; + + public PluginExecutor(LowcoderPlugin plugin, Map env, LowcoderServices services) + { + this.env = env; + this.plugin = plugin; + this.services = services; + this.setContextClassLoader(plugin.getClass().getClassLoader()); + this.setName(plugin.pluginId()); + } + + @Override + public void run() + { + if (plugin.load(env, services)) + { + log.info("Plugin [{}] loaded and running.", plugin.pluginId()); + } + } + + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java new file mode 100644 index 000000000..25ed33eb4 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/PluginLoader.java @@ -0,0 +1,11 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.List; + +import org.lowcoder.plugin.api.LowcoderPlugin; + +public interface PluginLoader +{ + List loadPlugins(); + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java new file mode 100644 index 000000000..1cd455e20 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/SharedPluginServices.java @@ -0,0 +1,59 @@ +package org.lowcoder.api.framework.plugin; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; + +import org.lowcoder.api.framework.plugin.endpoint.PluginEndpointHandler; +import org.lowcoder.infra.config.repository.ServerConfigRepository; +import org.lowcoder.plugin.api.LowcoderServices; +import org.lowcoder.plugin.api.PluginEndpoint; +import org.lowcoder.plugin.api.event.LowcoderEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Component +public class SharedPluginServices implements LowcoderServices +{ + private final PluginEndpointHandler pluginEndpointHandler; + + @Autowired + private ServerConfigRepository serverConfigRepository; + + private List> eventListeners = new LinkedList<>(); + + @Override + public void registerEventListener(Consumer listener) + { + this.eventListeners.add(listener); + } + + @EventListener(classes = LowcoderEvent.class) + private void publishEvents(LowcoderEvent event) + { + for (Consumer listener : eventListeners) + { + listener.accept(event); + } + } + + @Override + public void registerEndpoints(String urlPrefix, List endpoints) + { + pluginEndpointHandler.registerEndpoints(urlPrefix, endpoints); + } + + @Override + public void setConfig(String key, Object value) { + serverConfigRepository.upsert(key, value).block(); + } + + @Override + public Object getConfig(String key) { + return serverConfigRepository.findByKey(key).block(); + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java new file mode 100644 index 000000000..aa75bdc17 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/data/PluginServerRequest.java @@ -0,0 +1,198 @@ +package org.lowcoder.api.framework.plugin.data; + +import org.lowcoder.plugin.api.PluginEndpoint; +import org.lowcoder.plugin.api.PluginEndpoint.Method; +import org.lowcoder.plugin.api.data.EndpointRequest; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.web.reactive.function.server.ServerRequest; + +import java.net.URI; +import java.security.Principal; +import java.util.AbstractMap.SimpleEntry; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; + +public class PluginServerRequest implements EndpointRequest +{ + private URI uri; + private PluginEndpoint.Method method; + private CompletableFuture body; + private Map> headers; + private Map>> cookies; + private Map attributes; + private Map pathVariables; + + private Map> queryParams; + private CompletableFuture principal; + + + public PluginServerRequest() + { + headers = new HashMap<>(); + cookies = new HashMap<>(); + attributes = new HashMap<>(); + pathVariables = new HashMap<>(); + queryParams = new HashMap<>(); + } + + public static PluginServerRequest fromServerRequest(ServerRequest request) + { + PluginServerRequest psr = new PluginServerRequest(); + + psr.uri = request.uri(); + psr.method = fromHttpMetod(request.method()); + psr.body = request.bodyToMono(byte[].class).toFuture(); + + if (request.headers() != null) + { + HttpHeaders httpHeaders = request.headers().asHttpHeaders(); + psr.headers = httpHeaders; + } + + if (request.cookies() != null) + { + request.cookies().entrySet().stream() + .forEach(entry -> { + psr.cookies.put(entry.getKey(), fromHttpCookieList(entry.getValue())); + }); + } + + if (request.attributes() != null) + { + request.attributes().forEach((name, value) -> { + psr.attributes.put(name, value); + }); + } + + if (request.pathVariables() != null) + { + request.pathVariables().entrySet() + .forEach(entry -> { + psr.pathVariables.put(entry.getKey(), entry.getValue()); + }); + } + + if (request.queryParams() != null) + { + request.queryParams().entrySet() + .forEach(entry -> { + psr.queryParams.put(entry.getKey(), entry.getValue()); + }); + } + + psr.principal = request.principal().toFuture(); + + return psr; + } + + private static List> fromHttpCookieList(List cookies) + { + List> list = new LinkedList<>(); + + if (cookies != null) + { + cookies.stream() + .forEach(cookie -> { + list.add(new SimpleEntry(cookie.getName(), cookie.getValue())); + }); + } + + return list; + } + + + + @Override + public URI uri() { + return uri; + } + @Override + public Method method() { + return method; + } + @Override + public CompletableFuture body() { + return body; + } + @Override + public Map> headers() { + return headers; + } + @Override + public Map>> cookies() { + return cookies; + } + @Override + public Map attributes() { + return attributes; + } + @Override + public Map pathVariables() { + return pathVariables; + } + + @Override + public Map> queryParams() { + return queryParams; + } + @Override + public CompletableFuture principal() { + return principal; + } + + + public static HttpMethod fromPluginEndpointMethod(PluginEndpoint.Method method) + { + switch(method) + { + case GET: + return HttpMethod.GET; + case POST: + return HttpMethod.POST; + case PUT: + return HttpMethod.PUT; + case PATCH: + return HttpMethod.PATCH; + case DELETE: + return HttpMethod.DELETE; + case OPTIONS: + return HttpMethod.OPTIONS; + } + return null; + } + + public static PluginEndpoint.Method fromHttpMetod(HttpMethod method) + { + if (method == HttpMethod.GET) + { + return PluginEndpoint.Method.GET; + } + else if (method == HttpMethod.POST) + { + return PluginEndpoint.Method.POST; + } + else if (method == HttpMethod.PUT) + { + return PluginEndpoint.Method.PUT; + } + else if (method == HttpMethod.PATCH) + { + return PluginEndpoint.Method.PATCH; + } + else if (method == HttpMethod.DELETE) + { + return PluginEndpoint.Method.DELETE; + } + else if (method == HttpMethod.OPTIONS) + { + return PluginEndpoint.Method.OPTIONS; + } + return null; + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java new file mode 100644 index 000000000..11922c3dd --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandler.java @@ -0,0 +1,15 @@ +package org.lowcoder.api.framework.plugin.endpoint; + +import java.util.List; + +import org.lowcoder.plugin.api.PluginEndpoint; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +public interface PluginEndpointHandler +{ + public static final String PLUGINS_BASE_URL = "/api/plugins/"; + + void registerEndpoints(String urlPrefix, List endpoints); + List> registeredEndpoints(); +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java new file mode 100644 index 000000000..214252827 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/PluginEndpointHandlerImpl.java @@ -0,0 +1,198 @@ +package org.lowcoder.api.framework.plugin.endpoint; + +import static org.springframework.web.reactive.function.server.RequestPredicates.DELETE; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.OPTIONS; +import static org.springframework.web.reactive.function.server.RequestPredicates.PATCH; +import static org.springframework.web.reactive.function.server.RequestPredicates.POST; +import static org.springframework.web.reactive.function.server.RequestPredicates.PUT; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.lowcoder.api.framework.plugin.data.PluginServerRequest; +import org.lowcoder.api.framework.plugin.security.SecuredEndpoint; +import org.lowcoder.plugin.api.EndpointExtension; +import org.lowcoder.plugin.api.PluginEndpoint; +import org.lowcoder.plugin.api.data.EndpointRequest; +import org.lowcoder.plugin.api.data.EndpointResponse; +import org.lowcoder.sdk.exception.BaseException; +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.ProxyFactoryBean; +import org.springframework.aop.target.SimpleBeanTargetSource; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.http.ResponseCookie; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.RequestPredicate; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Slf4j +@RequiredArgsConstructor +@Component +public class PluginEndpointHandlerImpl implements PluginEndpointHandler +{ + private List> routes = new ArrayList<>(); + + private final ApplicationContext applicationContext; + private final DefaultListableBeanFactory beanFactory; + + @Override + public void registerEndpoints(String pluginUrlPrefix, List endpoints) + { + String urlPrefix = PLUGINS_BASE_URL + pluginUrlPrefix; + + if (CollectionUtils.isNotEmpty(endpoints)) + { + for (PluginEndpoint endpoint : endpoints) + { + Method[] handlers = endpoint.getClass().getDeclaredMethods(); + if (handlers != null && handlers.length > 0) + { + for (Method handler : handlers) + { + registerEndpointHandler(urlPrefix, endpoint, handler); + } + } + } + + ((ReloadableRouterFunctionMapping)beanFactory.getBean("routerFunctionMapping")).reloadFunctionMappings(); + } + } + + @Override + public List> registeredEndpoints() + { + return routes; + } + + private void registerEndpointHandler(String urlPrefix, PluginEndpoint endpoint, Method handler) + { + if (!handler.isAnnotationPresent(EndpointExtension.class) || !checkHandlerMethod(handler)) + { + if (handler.isAnnotationPresent(EndpointExtension.class)) + { + log.debug("Not registering plugin endpoint method: {} -> {}! Handler method must be defined as: public EndpointResponse methodName(EndpointRequest request)", endpoint.getClass().getSimpleName(), handler.getName(), handler.getName()); + } + return; + } + + EndpointExtension endpointMeta = handler.getAnnotation(EndpointExtension.class); + String endpointName = endpoint.getClass().getSimpleName() + "_" + handler.getName(); + RouterFunction routerFunction = route(createRequestPredicate(urlPrefix, endpointMeta), req -> runPluginEndpointMethod(endpoint, endpointMeta, handler, req)); + routes.add(routerFunction); + registerRouterFunctionMapping(endpointName, routerFunction); + + log.info("Registered endpoint: {} -> {}: {}", endpoint.getClass().getSimpleName(), endpointMeta.method(), urlPrefix + endpointMeta.uri()); + } + + @SecuredEndpoint + public Mono runPluginEndpointMethod(PluginEndpoint endpoint, EndpointExtension endpointMeta, Method handler, ServerRequest request) + { + Mono result = null; + try + { + log.info("Running plugin endpoint method {}\nRequest: {}", handler.getName(), request); + + EndpointResponse response = (EndpointResponse)handler.invoke(endpoint, PluginServerRequest.fromServerRequest(request)); + result = createServerResponse(response); + } + catch (IllegalAccessException | InvocationTargetException cause) + { + throw new BaseException("Error running handler for [ " + endpointMeta.method() + ": " + endpointMeta.uri() + "] !"); + } + return result; + } + + + private void registerRouterFunctionMapping(String endpointName, RouterFunction routerFunction) + { + String beanName = "pluginEndpoint_" + endpointName + "_" + System.currentTimeMillis(); + ((GenericApplicationContext)applicationContext).registerBean(beanName, RouterFunction.class, () -> routerFunction ); + log.debug("Registering RouterFunction bean definition: {}", beanName); + } + + + private Mono createServerResponse(EndpointResponse pluginResponse) + { + /** Create response with given status **/ + BodyBuilder builder = ServerResponse.status(pluginResponse.statusCode()); + + /** Set response headers **/ + if (pluginResponse.headers() != null && !pluginResponse.headers().isEmpty()) + { + pluginResponse.headers().entrySet() + .forEach(entry -> builder.header(entry.getKey(), entry.getValue().toArray(new String[] {}))); + } + + /** Set cookies if available **/ + if (pluginResponse.cookies() != null && !pluginResponse.cookies().isEmpty()) + { + pluginResponse.cookies().values() + .forEach(cookies -> cookies + .forEach(cookie -> builder + .cookie(ResponseCookie.from(cookie.getKey(), cookie.getValue()).build()))); + } + + /** Set response body if available **/ + if (pluginResponse.body() != null) + { + return builder.bodyValue(pluginResponse.body()); + } + + return builder.build(); + } + + private boolean checkHandlerMethod(Method method) + { + ResolvableType returnType = ResolvableType.forMethodReturnType(method); + + return (returnType.getRawClass().isAssignableFrom(EndpointResponse.class) + && method.getParameterCount() == 1 + && method.getParameterTypes()[0].isAssignableFrom(EndpointRequest.class) + ); + } + + private RequestPredicate createRequestPredicate(String basePath, EndpointExtension endpoint) + { + switch(endpoint.method()) + { + case GET: + return GET(pluginEndpointUri(basePath, endpoint.uri())); + case POST: + return POST(pluginEndpointUri(basePath, endpoint.uri())); + case PUT: + return PUT(pluginEndpointUri(basePath, endpoint.uri())); + case PATCH: + return PATCH(pluginEndpointUri(basePath, endpoint.uri())); + case DELETE: + return DELETE(pluginEndpointUri(basePath, endpoint.uri())); + case OPTIONS: + return OPTIONS(pluginEndpointUri(basePath, endpoint.uri())); + } + return null; + } + + private String pluginEndpointUri(String basePath, String uri) + { + return StringUtils.join(basePath, StringUtils.prependIfMissing(uri, "/")); + } + + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java new file mode 100644 index 000000000..42e8e5690 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/endpoint/ReloadableRouterFunctionMapping.java @@ -0,0 +1,20 @@ +package org.lowcoder.api.framework.plugin.endpoint; + +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.support.RouterFunctionMapping; + + +public class ReloadableRouterFunctionMapping extends RouterFunctionMapping +{ + /** + * Rescan application context for RouterFunction beans + */ + public void reloadFunctionMappings() + { + initRouterFunctions(); + if (getRouterFunction() != null) + { + RouterFunctions.changeParser(getRouterFunction(), getPathPatternParser()); + } + } +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java new file mode 100644 index 000000000..6ad509044 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/EndpointAuthorizationManager.java @@ -0,0 +1,24 @@ +package org.lowcoder.api.framework.plugin.security; + +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class EndpointAuthorizationManager implements AuthorizationManager +{ + + @Override + public AuthorizationDecision check(Supplier authentication, MethodInvocation invocation) + { + log.info("Checking plugin endpoint invocation security for {}", invocation.getMethod().getName()); + + return new AuthorizationDecision(true); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java new file mode 100644 index 000000000..237567643 --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/PluginAuthorizationManager.java @@ -0,0 +1,92 @@ +package org.lowcoder.api.framework.plugin.security; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.lang3.StringUtils; +import org.lowcoder.plugin.api.EndpointExtension; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.ExpressionAuthorizationDecision; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +@Slf4j +//@Component +public class PluginAuthorizationManager implements ReactiveAuthorizationManager +{ + private final MethodSecurityExpressionHandler expressionHandler; + + public PluginAuthorizationManager() + { + this.expressionHandler = new DefaultMethodSecurityExpressionHandler(); + } + + @Override + public Mono check(Mono authentication, MethodInvocation invocation) + { + log.info("Checking plugin reactive endpoint invocation security for {}", invocation.getMethod().getName()); + + EndpointExtension endpointExtension = (EndpointExtension)invocation.getArguments()[1]; + if (endpointExtension == null || StringUtils.isBlank(endpointExtension.authorize())) + { + return Mono.empty(); + } + + Expression authorizeExpression = this.expressionHandler.getExpressionParser() + .parseExpression(endpointExtension.authorize()); + + return authentication + .map(auth -> expressionHandler.createEvaluationContext(auth, invocation)) + .flatMap(ctx -> evaluateAsBoolean(authorizeExpression, ctx)) + .map(granted -> new ExpressionAuthorizationDecision(granted, authorizeExpression)); + } + + + private Mono evaluateAsBoolean(Expression expr, EvaluationContext ctx) + { + return Mono.defer(() -> + { + Object value; + try + { + value = expr.getValue(ctx); + } + catch (EvaluationException ex) + { + return Mono.error(() -> new IllegalArgumentException( + "Failed to evaluate expression '" + expr.getExpressionString() + "'", ex)); + } + + if (value instanceof Boolean bool) + { + return Mono.just(bool); + } + + if (value instanceof Mono monoBool) + { + Mono monoValue = monoBool; + return monoValue + .filter(Boolean.class::isInstance) + .map(Boolean.class::cast) + .switchIfEmpty(createInvalidReturnTypeMono(expr)); + } + return createInvalidReturnTypeMono(expr); + }); + } + + private static Mono createInvalidReturnTypeMono(Expression expr) + { + return Mono.error(() -> new IllegalStateException( + "Expression: '" + expr.getExpressionString() + "' must return boolean or Mono")); + } + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java new file mode 100644 index 000000000..aadc0c7fd --- /dev/null +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/plugin/security/SecuredEndpoint.java @@ -0,0 +1,16 @@ +package org.lowcoder.api.framework.plugin.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface SecuredEndpoint { + +} diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/framework/security/SecurityConfig.java index b933a63e1..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 69e4517d5..fcb066195 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderApiService.java @@ -182,6 +182,11 @@ private Mono removePermissions(String folderId) { public Mono update(Folder folder) { Folder newFolder = new Folder(); newFolder.setName(folder.getName()); + newFolder.setTitle(folder.getTitle()); + newFolder.setType(folder.getType()); + newFolder.setCategory(folder.getCategory()); + newFolder.setDescription(folder.getDescription()); + newFolder.setImage(folder.getImage()); return checkManagePermission(folder.getId()) .then(folderService.updateById(folder.getId(), newFolder)) .then(folderService.findById(folder.getId())) @@ -241,7 +246,7 @@ public Flux getElements(@Nullable String folderId, @Nullable ApplicationType if (folderInfoView == null) { return; } - folderInfoView.setManageable(orgMember.isAdmin() || orgMember.getUserId().equals(folderInfoView.getCreateBy())); + folderInfoView.setManageable(orgMember.isAdmin() || orgMember.isSuperAdmin() || orgMember.getUserId().equals(folderInfoView.getCreateBy())); List folderInfoViews = folderNode.getFolderChildren().stream().filter(FolderInfoView::isVisible).toList(); folderInfoView.setSubFolders(folderInfoViews); @@ -335,7 +340,7 @@ private Mono> buildApplicationInfoView private Mono checkManagePermission(String folderId) { return sessionUserService.getVisitorOrgMemberCache() .flatMap(orgMember -> { - if (orgMember.isAdmin()) { + if (orgMember.isAdmin() || orgMember.isSuperAdmin()) { return Mono.just(orgMember); } return isCreator(folderId) @@ -421,6 +426,10 @@ public Mono buildFolderInfoView(Folder folder, boolean visible, .folderId(folder.getId()) .parentFolderId(folder.getParentFolderId()) .name(folder.getName()) + .description(folder.getDescription()) + .category(folder.getCategory()) + .type(folder.getType()) + .image(folder.getImage()) .createAt(folder.getCreatedAt() == null ? 0 : folder.getCreatedAt().toEpochMilli()) .createBy(user.getName()) .createTime(folder.getCreatedAt()) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java index ae7e2f2c0..4f07b0342 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java @@ -1,6 +1,6 @@ package org.lowcoder.api.home; -import static org.lowcoder.infra.event.EventType.APPLICATION_MOVE; +import static org.lowcoder.plugin.api.event.LowcoderEvent.EventType.APPLICATION_MOVE; import static org.lowcoder.sdk.exception.BizError.INVALID_PARAMETER; import static org.lowcoder.sdk.util.ExceptionUtils.ofError; @@ -13,7 +13,11 @@ import org.lowcoder.domain.folder.model.Folder; import org.lowcoder.domain.folder.service.FolderService; import org.lowcoder.domain.permission.model.ResourceRole; -import org.lowcoder.infra.event.EventType; +import org.lowcoder.infra.constant.NewUrl; +import org.lowcoder.plugin.api.event.LowcoderEvent.EventType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java index b1abb505f..17776f298 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderInfoView.java @@ -20,6 +20,11 @@ public class FolderInfoView { private final String folderId; private final String parentFolderId; private final String name; + private final String title; + private final String description; + private final String category; + private final String type; + private final String image; private final Long createAt; private final String createBy; private boolean isVisible; diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java index 9104839d9..a96485eae 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserService.java @@ -18,6 +18,8 @@ public interface SessionUserService { @NonEmptyMono Mono getVisitorOrgMemberCache(); + Mono getVisitorOrgMemberCacheSilent(); + Mono getVisitorOrgMember(); Mono isAnonymousUser(); @@ -33,4 +35,6 @@ public interface SessionUserService { Mono resolveSessionUserForJWT(Claims claims, String token); Mono tokenExist(String token); + + Mono getVisitorToken(); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java index 5c0b5e1fe..75b5bec8d 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/SessionUserServiceImpl.java @@ -1,6 +1,7 @@ package org.lowcoder.api.home; import static org.lowcoder.sdk.constants.GlobalContext.CURRENT_ORG_MEMBER; +import static org.lowcoder.sdk.constants.GlobalContext.VISITOR_TOKEN; import static org.lowcoder.sdk.exception.BizError.UNABLE_TO_FIND_VALID_ORG; import static org.lowcoder.sdk.util.ExceptionUtils.deferredError; import static org.lowcoder.sdk.util.JsonUtils.fromJsonQuietly; @@ -74,6 +75,17 @@ public Mono getVisitorOrgMemberCache() { .switchIfEmpty(deferredError(UNABLE_TO_FIND_VALID_ORG, "UNABLE_TO_FIND_VALID_ORG")); } + @Override + public Mono getVisitorOrgMemberCacheSilent() { + return Mono.deferContextual(contextView -> (Mono) contextView.get(CURRENT_ORG_MEMBER)) + .delayUntil(Mono::just); + } + + @Override + public Mono getVisitorToken() { + return Mono.deferContextual(contextView -> Mono.just(contextView.get(VISITOR_TOKEN))); + } + @Override public Mono getVisitorOrgMember() { return getVisitorId() diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/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..8ec6f774d 100644 --- a/server/api-service/pom.xml +++ b/server/api-service/pom.xml @@ -1,335 +1,151 @@ - - - - org.springframework.boot - spring-boot-starter-parent - 3.1.1 - - - - 4.0.0 - org.lowcoder - lowcoder-root - ${revision} - pom - lowcoder-root - - - 2.3.0-SNAPSHOT - 17 - true - true - true - org.lowcoder - 1.0-SNAPSHOT - true - 2.17.0 - 17 - 17 - - - - - sonatype - https://oss.sonatype.org/content/repositories/snapshots - - - - - - - cloud - - cloud - - - true - - - - - false - src/main/java - - **/*.java - - - - src/main/resources - - **/selfhost/application*.yml - - - - - - - selfhost - - selfhost - - - - - false - src/main/java - - **/*.java - - - - src/main/resources - - **/application*.yml - - - - - - - - - - - org.codehaus.mojo - license-maven-plugin - 2.0.0 - - - maven-dependency-plugin - 3.1.2 - - - - - - - + + + 4.0.0 + org.lowcoder + lowcoder-root + pom + lowcoder-root + ${revision} + + + + 2.4.0 + 17 + true + true + true + true + + + + + sonatype + https://oss.sonatype.org/content/repositories/snapshots + + + + + + + cloud + + cloud + + + true + + + + + false + src/main/java + + **/*.java + + + + src/main/resources + + **/selfhost/application*.yml + + + + + + + selfhost + + selfhost + + + + + false + src/main/java + + **/*.java + + + + src/main/resources + + **/application*.yml + + + + + + + + + + + org.codehaus.mojo + license-maven-plugin + 2.0.0 + + + maven-dependency-plugin + + + + + + maven-assembly-plugin + 3.6.0 + + + src/assembly/bin.xml + + + + + + + + + + org.lowcoder lowcoder-sdk ${revision} - + org.lowcoder lowcoder-infra ${revision} - + org.lowcoder lowcoder-domain ${revision} - + org.lowcoder lowcoder-plugins ${revision} - + org.lowcoder lowcoder-server ${revision} - - - - org.pf4j - pf4j - 3.5.0 - - - - org.json - json - 20230227 - - - - org.projectlombok - lombok - 1.18.26 - - - - org.apache.commons - commons-text - 1.10.0 - - - commons-io - commons-io - 2.13.0 - - - org.glassfish - javax.el - 3.0.0 - - - javax.el - javax.el-api - 3.0.0 - - - - org.eclipse.jgit - org.eclipse.jgit - 6.7.0.202309050840-r - - - - org.apache.commons - commons-collections4 - 4.4 - - - com.google.guava - guava - 30.0-jre - - - - tv.twelvetone.rjson - rjson - 1.3.1-SNAPSHOT - - - org.jetbrains.kotlin - kotlin-stdlib-jdk7 - 1.6.21 - - - - com.jayway.jsonpath - json-path - 2.7.0 - - - com.github.ben-manes.caffeine - caffeine - 3.0.5 - - - es.moki.ratelimitj - ratelimitj-core - 0.7.0 - - - com.github.spullara.mustache.java - compiler - 0.9.6 - - - - es.moki.ratelimitj - ratelimitj-redis - 0.7.0 - - - - io.projectreactor - reactor-core - 3.4.29 - - - - org.pf4j - pf4j-spring - 0.8.0 - - - - com.querydsl - querydsl-apt - 5.0.0 - - - - io.sentry - sentry-spring-boot-starter - 3.1.2 - - - - org.jgrapht - jgrapht-core - 1.5.0 - - - - javax.xml.bind - jaxb-api - 2.3.1 - - - javax.activation - activation - 1.1.1 - - - - org.glassfish.jaxb - jaxb-runtime - 2.3.3 - - - - com.github.cloudyrock.mongock - mongock-bom - 4.3.8 - pom - import - - - - io.projectreactor.tools - blockhound - 1.0.6.RELEASE - - - - jakarta.servlet - jakarta.servlet-api - 6.0.0 - - - io.projectreactor - reactor-test - 3.3.5.RELEASE - - - org.apache.httpcomponents - httpclient - 4.5.14 - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo.spring30x - 4.7.0 - - - org.mockito - mockito-inline - 5.2.0 - test - - - javax.validation - validation-api - 2.0.1.Final - - - - - - lowcoder-sdk - lowcoder-infra - lowcoder-domain - lowcoder-plugins - lowcoder-server - + + + + + lowcoder-dependencies + lowcoder-sdk + lowcoder-infra + lowcoder-domain + lowcoder-plugins + lowcoder-server + distribution + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy