diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..ec9a3f3 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,42 @@ +# .NET 9 SDK (use 8.0 if your solution targets 8) +# Force amd64 so IBM i Access RPM can be installed even on Apple Silicon hosts. +FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:9.0 + +# Java runtime is required by Sonar +RUN apt-get update && apt-get install -y --no-install-recommends \ + openjdk-17-jre-headless curl ca-certificates git unzip gnupg \ + unixodbc unixodbc-dev alien \ + && rm -rf /var/lib/apt/lists/* + +# Node.js 20.x is required by SonarLint for VS Code +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get update \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +# Install IBM i Access ODBC driver when the RPM is present in the repo +COPY drivers/ /tmp/drivers/ +RUN set -ex; \ + if ls /tmp/drivers/ibm-iaccess-*.rpm.disabled >/dev/null 2>&1; then \ + for f in /tmp/drivers/ibm-iaccess-*.rpm.disabled; do mv "$f" "${f%.disabled}"; done; \ + fi +RUN set -ex; \ + if ls /tmp/drivers/ibm-iaccess-*.rpm >/dev/null 2>&1; then \ + for f in /tmp/drivers/ibm-iaccess-*.rpm; do alien -i --scripts "$f"; done; \ + else \ + echo ">> IBM i Access ODBC RPM not found in /tmp/drivers. ODBC connections will fail until it is added."; \ + fi + +# Register ODBC driver/DSN so unixODBC knows about the IBM driver +COPY docker/odbc/odbcinst.ini /etc/odbcinst.ini +COPY docker/odbc/odbc.ini /etc/odbc.ini + +# Global tools: dotnet-sonarscanner (+ coverlet for coverage) +RUN dotnet tool install --global dotnet-sonarscanner \ + && dotnet tool install --global coverlet.console +ENV PATH="${PATH}:/root/.dotnet/tools" + +# Non-root user (Dev Containers convention) +RUN useradd -ms /bin/bash vscode +USER vscode +WORKDIR /workspaces diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..008a3b5 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,52 @@ +{ + "name": "AS400API Dev", + "build": { + "dockerfile": "./Dockerfile", + "context": ".." + }, + + + // "remoteUser": "vscode", + "remoteEnv": { + "SONAR_HOST_URL": "http://host.docker.internal:9000" + }, + + + "runArgs": ["--init"], + + // Ensure IBM i Access shared libraries are on the loader path when running in devcontainer + "containerEnv": { + "LD_LIBRARY_PATH": "/opt/ibm/iaccess/lib64:/opt/ibm/iaccess/lib", + "AS400_DRIVER_NAME": "IBM i Access ODBC Driver", + "SONAR_TOKEN": "squ_ef2f0a2f495a32c33ed81afb16f3cdc98bf1336a" + }, + + // เมานท์โฟลเดอร์งานเข้า /workspaces/AS400API + "workspaceFolder": "/workspaces/AS400API", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/AS400API,type=bind,consistency=cached", + + "forwardPorts": [8080], + "postCreateCommand": "apt-get update && apt-get install -y jq && dotnet --info && dotnet restore", + + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "dotnet.defaultSolution": "AS400API.sln", + "dotnet.projects.enableFileBasedPrograms": false + }, + + "customizations": { + "vscode": { + "extensions": [ + // "ms-dotnettools.csharp", + "ms-azuretools.vscode-docker", + "oderwat.indent-rainbow", + "streetsidesoftware.code-spell-checker", + "openai.chatgpt", + "ms-dotnettools.csdevkit", + "SonarSource.sonarlint-vscode" + ] + } + }, + + "remoteUser": "root" +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a301b6c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +**/bin/ +**/obj/ +**/.vs/ +**/.vscode/ +**/.idea/ +**/.DS_Store +*.zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e9060 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin +Logs +obj diff --git a/.sonarqube/conf/0/FilesToAnalyze.txt b/.sonarqube/conf/0/FilesToAnalyze.txt new file mode 100644 index 0000000..84c89a1 --- /dev/null +++ b/.sonarqube/conf/0/FilesToAnalyze.txt @@ -0,0 +1,35 @@ +/workspaces/AS400API/Auth/AuthPolicies.cs +/workspaces/AS400API/Auth/DemoUser.cs +/workspaces/AS400API/Auth/DemoUserStore.cs +/workspaces/AS400API/Auth/LoginRequest.cs +/workspaces/AS400API/Auth/LoginResponse.cs +/workspaces/AS400API/Auth/PasswordHasher.cs +/workspaces/AS400API/Auth/Roles.cs +/workspaces/AS400API/Auth/TokenService.cs +/workspaces/AS400API/Configuration/JwtOptions.cs +/workspaces/AS400API/Configuration/OdbcOptions.cs +/workspaces/AS400API/Endpoints/As400Endpoints.cs +/workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs +/workspaces/AS400API/Endpoints/AuthEndpoints.cs +/workspaces/AS400API/Endpoints/SystemEndpoints.cs +/workspaces/AS400API/Infrastructure/DatabaseRowFormatter.cs +/workspaces/AS400API/Infrastructure/DataReaderExtensions.cs +/workspaces/AS400API/Program.cs +/workspaces/AS400API/obj/Debug/net9.0/AS400API.GlobalUsings.g.cs +/workspaces/AS400API/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs +/workspaces/AS400API/obj/Debug/net9.0/AS400API.AssemblyInfo.cs +/workspaces/AS400API/obj/Debug/net9.0/AS400API.MvcApplicationPartsAssemblyInfo.cs +/workspaces/AS400API/appsettings.json +/workspaces/AS400API/.dockerignore +/workspaces/AS400API/docker-compose.yml +/workspaces/AS400API/docker/odbc/odbc.ini +/workspaces/AS400API/docker/odbc/odbcinst.ini +/workspaces/AS400API/Dockerfile +/workspaces/AS400API/drivers/ibm-iaccess-1.1.0.28-1.0.x86_64.rpm +/workspaces/AS400API/Logs/as400-api-20251001.log +/workspaces/AS400API/Logs/as400-api-20251002.log +/workspaces/AS400API/Logs/as400-api-20251003.log +/workspaces/AS400API/Logs/as400-api-20251004.log +/workspaces/AS400API/README.md +/workspaces/AS400API/scripts/run-sonar.sh +/workspaces/AS400API/scripts/test-databases.sh diff --git a/.sonarqube/conf/0/ProjectOutFolderPath.txt b/.sonarqube/conf/0/ProjectOutFolderPath.txt new file mode 100644 index 0000000..219412b --- /dev/null +++ b/.sonarqube/conf/0/ProjectOutFolderPath.txt @@ -0,0 +1 @@ +/workspaces/AS400API/.sonarqube/out/0 diff --git a/.sonarqube/conf/0/SonarProjectConfig.xml b/.sonarqube/conf/0/SonarProjectConfig.xml new file mode 100644 index 0000000..9e3f838 --- /dev/null +++ b/.sonarqube/conf/0/SonarProjectConfig.xml @@ -0,0 +1,9 @@ + + + /workspaces/AS400API/.sonarqube/conf/SonarQubeAnalysisConfig.xml + /workspaces/AS400API/AS400API.csproj + /workspaces/AS400API/.sonarqube/conf/0/FilesToAnalyze.txt + /workspaces/AS400API/.sonarqube/out/0 + Product + net9.0 + \ No newline at end of file diff --git a/.sonarqube/conf/Sonar-cs-none.ruleset b/.sonarqube/conf/Sonar-cs-none.ruleset new file mode 100644 index 0000000..c9e8431 --- /dev/null +++ b/.sonarqube/conf/Sonar-cs-none.ruleset @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.sonarqube/conf/Sonar-cs.ruleset b/.sonarqube/conf/Sonar-cs.ruleset new file mode 100644 index 0000000..92ad81f --- /dev/null +++ b/.sonarqube/conf/Sonar-cs.ruleset @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.sonarqube/conf/Sonar-vbnet-none.ruleset b/.sonarqube/conf/Sonar-vbnet-none.ruleset new file mode 100644 index 0000000..2d27b76 --- /dev/null +++ b/.sonarqube/conf/Sonar-vbnet-none.ruleset @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.sonarqube/conf/Sonar-vbnet.ruleset b/.sonarqube/conf/Sonar-vbnet.ruleset new file mode 100644 index 0000000..bf6aca3 --- /dev/null +++ b/.sonarqube/conf/Sonar-vbnet.ruleset @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.sonarqube/conf/SonarQubeAnalysisConfig.xml b/.sonarqube/conf/SonarQubeAnalysisConfig.xml new file mode 100644 index 0000000..9bbceae --- /dev/null +++ b/.sonarqube/conf/SonarQubeAnalysisConfig.xml @@ -0,0 +1,172 @@ + + + /workspaces/AS400API/.sonarqube/conf + /workspaces/AS400API/.sonarqube/out + /workspaces/AS400API/.sonarqube/bin + /workspaces/AS400API + false + true + true + false + http://host.docker.internal:9000 + 9.9.8.100196 + as400api + AS400API + + + + + + + + + + 8.51.0.59060 + true + .ts,.tsx,.cts,.mts + SonarQube + false + false + **/vendor/** + .tf + true + 60 + SonarAnalyzer.CSharp + false + false + .css,.less,.scss + .html,.xhtml,.cshtml,.vbhtml,.aspx,.ascx,.rhtml,.erb,.shtm,.shtml,.cmp,.twig + false + false + true + false + .scala + true + SonarAnalyzer.VisualBasic + AWSTemplateFormatVersion + true + 30 + 52 + https://secure.gravatar.com/avatar/{EMAIL_MD5}.jpg?s={SIZE}&d=identicon + 600 + .jsp,.jspf,.jspx + 1000 + amd,applescript,atomtest,browser,commonjs,couch,embertest,flow,greasemonkey,jasmine,jest,jquery,meteor,mocha,mongo,nashorn,node,phantomjs,prototypejs,protractor,qunit,rhino,serviceworker,shared-node-browser,shelljs,webextensions,worker,wsh,yui + false + **/vendor/** + false + .vb + 30 + true + py + .cs + true + SonarAnalyzer-8.51.0.59060.zip + .java,.jav + .kt + php,php3,php4,php5,phtml,inc + .xml,.xsd,.xsl + 260 + 3600000 + true + false + coverage-reports/*coverage-*.xml + 8.51.0.59060 + .go + sonarqube + 104 + true + **/vendor/** + false + as + 20 + false + https://api.github.com/ + .rb + true + xunit-reports/xunit-result-*.xml + angular,goog,google,OenLayers,d3,dojo,dojox,dijit,Backbone,moment,casper,_,sap + 24 + .yaml,.yml + false + true + https://github.com/ + SonarAnalyzer.VisualBasic + noreply@nowhere + 8.51.0.59060 + 4 + SonarAnalyzer.VisualBasic + [SONARQUBE] + false + csharp + true + .json + SAML + coverage/.resultset.json + SonarAnalyzer-8.51.0.59060.zip + true + main + SonarAnalyzer.CSharp + false + SonarAnalyzer.CSharp + 0.05,0.1,0.2,0.5 + false + true + 8.51.0.59060 + false + .js,.jsx,.cjs,.mjs,.vue + false + NOT_ACCEPTED + https://gitlab.com + vbnet + https://update.sonarsource.org/update-center.properties + 147B411E-AZmtNwrV3lb8QGbhu0SU + 10/4/2025 3:17:58 AM + + + http://host.docker.internal:9000 + **/coverage.opencover.xml + **/bin/**,**/obj/**,**/coverage.opencover.xml + + + + + cs + /workspaces/AS400API/.sonarqube/conf/Sonar-cs.ruleset + /workspaces/AS400API/.sonarqube/conf/Sonar-cs-none.ruleset + + + + /tmp/.sonarqube/resources/0/SonarAnalyzer.CFG.dll + /tmp/.sonarqube/resources/0/Google.Protobuf.dll + /tmp/.sonarqube/resources/0/SonarAnalyzer.CSharp.dll + /tmp/.sonarqube/resources/0/SonarAnalyzer.dll + /tmp/.sonarqube/resources/0/THIRD-PARTY-NOTICES.txt + + + + + /workspaces/AS400API/.sonarqube/conf/cs/SonarLint.xml + + + + vbnet + /workspaces/AS400API/.sonarqube/conf/Sonar-vbnet.ruleset + /workspaces/AS400API/.sonarqube/conf/Sonar-vbnet-none.ruleset + + + + /tmp/.sonarqube/resources/1/SonarAnalyzer.CFG.dll + /tmp/.sonarqube/resources/1/SonarAnalyzer.VisualBasic.dll + /tmp/.sonarqube/resources/1/Google.Protobuf.dll + /tmp/.sonarqube/resources/1/SonarAnalyzer.dll + /tmp/.sonarqube/resources/1/THIRD-PARTY-NOTICES.txt + + + + + /workspaces/AS400API/.sonarqube/conf/vbnet/SonarLint.xml + + + + \ No newline at end of file diff --git a/.sonarqube/conf/cs/SonarLint.xml b/.sonarqube/conf/cs/SonarLint.xml new file mode 100644 index 0000000..9783ca7 --- /dev/null +++ b/.sonarqube/conf/cs/SonarLint.xml @@ -0,0 +1,869 @@ + + + + + sonar.cs.opencover.reportsPaths + **/coverage.opencover.xml + + + sonar.cs.ignoreHeaderComments + true + + + sonar.cs.analyzeGeneratedCode + false + + + sonar.cs.file.suffixes + .cs + + + sonar.cs.roslyn.ignoreIssues + false + + + + + S2225 + + + S2346 + + + S2223 + + + S2344 + + + S2589 + + + S3433 + + + S1134 + + + S1135 + + + S2345 + + + S4524 + + + S2342 + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + flagsAttributeFormat + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$ + + + + + S2222 + + + S2583 + + + S2115 + + + S3447 + + + S2234 + + + S2479 + + + S3444 + + + S3445 + + + S1144 + + + S1264 + + + S2114 + + + S3442 + + + S3440 + + + S3443 + + + S3329 + + + S3449 + + + S3655 + + + S3776 + + + propertyThreshold + 3 + + + threshold + 15 + + + + + S2688 + + + S3897 + + + S1110 + + + S2201 + + + S4502 + + + S1118 + + + S2328 + + + S2681 + + + S1117 + + + S2326 + + + S3415 + + + S4507 + + + S1116 + + + S1479 + + + maximum + 30 + + + + + S2699 + + + S1125 + + + S2696 + + + S4635 + + + S1121 + + + S1123 + + + S2219 + + + S2692 + + + S1006 + + + S1481 + + + S3427 + + + S3237 + + + S3358 + + + S3598 + + + S3236 + + + S5773 + + + S2386 + + + S3597 + + + S4200 + + + S4201 + + + S1172 + + + S4457 + + + S5659 + + + S3249 + + + S4456 + + + S3005 + + + S3246 + + + S3247 + + + S4211 + + + S5547 + + + S3244 + + + S5542 + + + S1066 + + + S1186 + + + S4210 + + + S1185 + + + S2275 + + + S3241 + + + S2368 + + + S3457 + + + S4423 + + + S1155 + + + S2245 + + + S3456 + + + S3458 + + + S4426 + + + S2123 + + + S2365 + + + S2486 + + + S3453 + + + S3330 + + + S3451 + + + S5753 + + + S3217 + + + S3459 + + + S4428 + + + S3218 + + + S927 + + + S2259 + + + S3450 + + + S5766 + + + S1048 + + + S1168 + + + S2257 + + + S3466 + + + S2376 + + + S3343 + + + S3346 + + + S3464 + + + S2252 + + + S3220 + + + S4433 + + + S1163 + + + S4790 + + + S818 + + + S2251 + + + S2372 + + + S4792 + + + S2743 + + + S1656 + + + S907 + + + S2995 + + + S3600 + + + S3963 + + + S2996 + + + S2757 + + + S2755 + + + S2997 + + + S3604 + + + S1751 + + + S3603 + + + S3966 + + + S1643 + + + S1871 + + + S1764 + + + S2737 + + + S2971 + + + S2612 + + + S2857 + + + S3875 + + + S1210 + + + S3871 + + + S1450 + + + S2306 + + + S3877 + + + S1104 + + + S1215 + + + S1699 + + + S3998 + + + S3400 + + + S3884 + + + S3887 + + + S2551 + + + S3885 + + + S2436 + + + maxMethod + 3 + + + max + 2 + + + + + S3881 + + + S2437 + + + S3889 + + + S1313 + + + S3610 + + + S3972 + + + S3973 + + + S2761 + + + S3971 + + + S3984 + + + S4830 + + + S3981 + + + S1206 + + + S3626 + + + S3869 + + + S1940 + + + S1944 + + + S1905 + + + S1939 + + + S5034 + + + S4061 + + + S5042 + + + S1854 + + + S4070 + + + S1862 + + + S3927 + + + S3928 + + + S3925 + + + S3926 + + + S2953 + + + S3923 + + + S125 + + + S1607 + + + S1848 + + + S2930 + + + S3903 + + + S3904 + + + S2933 + + + S2934 + + + S6422 + + + S110 + + + max + 5 + + + + + S2068 + + + credentialWords + password, passwd, pwd, passphrase + + + + + S5332 + + + S112 + + + S2187 + + + S4487 + + + S6424 + + + S3397 + + + S2184 + + + S5693 + + + fileUploadSizeLimit + 8000000 + + + + + S6420 + + + S107 + + + max + 7 + + + + + S2183 + + + S108 + + + S4019 + + + S3169 + + + S3168 + + + S4015 + + + S4136 + + + S2077 + + + S2190 + + + S1199 + + + S3376 + + + S1075 + + + S3011 + + + S3256 + + + S4581 + + + S4586 + + + S3010 + + + S3251 + + + S4220 + + + S4583 + + + S5443 + + + S2178 + + + S3264 + + + S3267 + + + S101 + + + S5445 + + + S3262 + + + S3265 + + + S6419 + + + S2053 + + + S2292 + + + S3263 + + + S3260 + + + S3261 + + + S2290 + + + S2291 + + + S6444 + + + S4144 + + + S4143 + + + S3172 + + + S4159 + + + S4260 + + + S4036 + + + S4277 + + + S4035 + + + S4158 + + + S4275 + + + S2092 + + + S3060 + + + S5122 + + + + + diff --git a/.sonarqube/conf/vbnet/SonarLint.xml b/.sonarqube/conf/vbnet/SonarLint.xml new file mode 100644 index 0000000..0521585 --- /dev/null +++ b/.sonarqube/conf/vbnet/SonarLint.xml @@ -0,0 +1,477 @@ + + + + + sonar.vbnet.ignoreHeaderComments + true + + + sonar.vbnet.file.suffixes + .vb + + + sonar.vbnet.roslyn.ignoreIssues + false + + + sonar.vbnet.analyzeGeneratedCode + false + + + + + S1654 + + + format + ^[a-z][a-z0-9]*([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S2225 + + + S1135 + + + S2344 + + + S2346 + + + S2347 + + + format + ^(([a-z][a-z0-9]*)?([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?_)?([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S1134 + + + S2342 + + + flagsAttributeFormat + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?s$ + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S2345 + + + S2068 + + + credentialWords + password, passwd, pwd, passphrase + + + + + S2222 + + + S6146 + + + S112 + + + S2340 + + + S1656 + + + S2349 + + + S907 + + + S6145 + + + S5693 + + + fileUploadSizeLimit + 8000000 + + + + + S107 + + + max + 7 + + + + + S108 + + + S1940 + + + S2358 + + + S1542 + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S2234 + + + S2355 + + + S4136 + + + S2077 + + + S2352 + + + S2359 + + + S2757 + + + S3449 + + + S114 + + + format + ^I([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S117 + + + format + ^[a-z][a-z0-9]*([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S3603 + + + S3776 + + + propertyThreshold + 3 + + + threshold + 15 + + + + + S1110 + + + S1751 + + + S1871 + + + S1075 + + + S1197 + + + S3011 + + + S4586 + + + S1479 + + + maximum + 30 + + + + + S4507 + + + S4581 + + + S1643 + + + S4583 + + + S1123 + + + S1125 + + + S1764 + + + S101 + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$ + + + + + S2178 + + + S5443 + + + S5445 + + + S2692 + + + S2737 + + + S3385 + + + S1481 + + + S1645 + + + S2612 + + + S3358 + + + S5042 + + + S2387 + + + S3598 + + + S3871 + + + S4201 + + + S1172 + + + S2304 + + + format + ^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?(\.([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?)*$ + + + + + S3998 + + + S5659 + + + S1862 + + + S2951 + + + S5944 + + + S3884 + + + S5547 + + + S1066 + + + S2551 + + + S4210 + + + S5542 + + + S1186 + + + S3923 + + + S3926 + + + S3927 + + + S2437 + + + S3889 + + + S1313 + + + S2368 + + + S4423 + + + S1155 + + + S6444 + + + S2761 + + + S3453 + + + S2365 + + + S4144 + + + S5753 + + + S4428 + + + S927 + + + S4143 + + + S4260 + + + S1048 + + + S2259 + + + S4159 + + + S4830 + + + S2257 + + + S2375 + + + minimumSeriesLength + 6 + + + + + S3466 + + + S4036 + + + S3464 + + + S3981 + + + S4277 + + + S2376 + + + S1163 + + + S3903 + + + S3904 + + + S2372 + + + S3869 + + + S4790 + + + S4275 + + + S4792 + + + + + diff --git a/.sonarqube/out/.sonar/.sonar_lock b/.sonarqube/out/.sonar/.sonar_lock new file mode 100644 index 0000000..e69de29 diff --git a/.sonarqube/out/.sonar/report-task.txt b/.sonarqube/out/.sonar/report-task.txt new file mode 100644 index 0000000..5a5c515 --- /dev/null +++ b/.sonarqube/out/.sonar/report-task.txt @@ -0,0 +1,6 @@ +projectKey=as400api +serverUrl=http://host.docker.internal:9000 +serverVersion=9.9.8.100196 +dashboardUrl=http://host.docker.internal:9000/dashboard?id=as400api +ceTaskId=AZmtj01mSkXasGRcHif1 +ceTaskUrl=http://host.docker.internal:9000/api/ce/task?id=AZmtj01mSkXasGRcHif1 diff --git a/.sonarqube/out/0/Issues.json b/.sonarqube/out/0/Issues.json new file mode 100644 index 0000000..c134e37 --- /dev/null +++ b/.sonarqube/out/0/Issues.json @@ -0,0 +1,625 @@ +{ + "$schema": "http://json.schemastore.org/sarif-1.0.0", + "version": "1.0.0", + "runs": [ + { + "tool": { + "name": "Microsoft (R) Visual C# Compiler", + "version": "4.14.0.0", + "fileVersion": "4.14.0-3.25413.5 (b828a8df)", + "semanticVersion": "4.14.0", + "language": "en-US" + }, + "results": [ + { + "ruleId": "S125", + "level": "warning", + "message": "Remove this commented out code.", + "locations": [ + { + "resultFile": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 147, + "startColumn": 17, + "endLine": 147, + "endColumn": 97 + } + } + } + ], + "properties": { + "warningLevel": 1 + } + }, + { + "ruleId": "S125", + "level": "warning", + "message": "Remove this commented out code.", + "locations": [ + { + "resultFile": { + "uri": "file:///workspaces/AS400API/Program.cs", + "region": { + "startLine": 11, + "startColumn": 1, + "endLine": 11, + "endColumn": 53 + } + } + } + ], + "properties": { + "warningLevel": 1 + } + }, + { + "ruleId": "S2068", + "level": "warning", + "message": "\"password\" detected here, make sure this is not a hard-coded credential.", + "locations": [ + { + "resultFile": { + "uri": "file:///workspaces/AS400API/appsettings.json", + "region": { + "startLine": 6, + "startColumn": 18, + "endLine": 6, + "endColumn": 24 + } + } + } + ], + "properties": { + "warningLevel": 1 + } + }, + { + "ruleId": "S1144", + "level": "warning", + "message": "Remove the unused private field 'LibraryNamePattern'.", + "locations": [ + { + "resultFile": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 152, + "startColumn": 5, + "endLine": 152, + "endColumn": 125 + } + } + } + ], + "properties": { + "warningLevel": 1 + } + }, + { + "ruleId": "S101", + "level": "warning", + "message": "Rename class 'ORDUAGEndpoint' to match pascal case naming rules, consider using 'OrduagEndpoint'.", + "locations": [ + { + "resultFile": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 15, + "startColumn": 21, + "endLine": 15, + "endColumn": 35 + } + } + } + ], + "properties": { + "warningLevel": 1 + } + }, + { + "ruleId": "S6444", + "level": "warning", + "message": "Pass a timeout to limit the execution time.", + "locations": [ + { + "resultFile": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 152, + "startColumn": 56, + "endLine": 152, + "endColumn": 124 + } + } + } + ], + "properties": { + "warningLevel": 1 + } + }, + { + "ruleId": "SYSLIB1045", + "level": "note", + "message": "Use 'GeneratedRegexAttribute' to generate the regular expression implementation at compile-time.", + "locations": [ + { + "resultFile": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 152, + "startColumn": 56, + "endLine": 152, + "endColumn": 124 + } + } + } + ], + "properties": { + "warningLevel": 1 + } + }, + { + "ruleId": "S3776", + "level": "warning", + "message": "Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.", + "locations": [ + { + "resultFile": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 16, + "startColumn": 37, + "endLine": 16, + "endColumn": 54 + } + } + } + ], + "relatedLocations": [ + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 24, + "startColumn": 17, + "endLine": 24, + "endColumn": 19 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 36, + "startColumn": 13, + "endLine": 36, + "endColumn": 18 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 59, + "startColumn": 13, + "endLine": 59, + "endColumn": 18 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 74, + "startColumn": 17, + "endLine": 74, + "endColumn": 19 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 92, + "startColumn": 13, + "endLine": 92, + "endColumn": 18 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 107, + "startColumn": 17, + "endLine": 107, + "endColumn": 19 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 107, + "startColumn": 60, + "endLine": 107, + "endColumn": 62 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 135, + "startColumn": 21, + "endLine": 135, + "endColumn": 26 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 138, + "startColumn": 25, + "endLine": 138, + "endColumn": 28 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/As400Endpoints.cs", + "region": { + "startLine": 154, + "startColumn": 13, + "endLine": 154, + "endColumn": 18 + } + } + } + ], + "properties": { + "warningLevel": 1, + "customProperties": { + "0": "+2 (incl 1 for nesting)", + "1": "+2 (incl 1 for nesting)", + "2": "+2 (incl 1 for nesting)", + "3": "+2 (incl 1 for nesting)", + "4": "+2 (incl 1 for nesting)", + "5": "+2 (incl 1 for nesting)", + "6": "+1", + "7": "+2 (incl 1 for nesting)", + "8": "+3 (incl 2 for nesting)", + "9": "+2 (incl 1 for nesting)" + } + } + }, + { + "ruleId": "S3776", + "level": "warning", + "message": "Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed.", + "locations": [ + { + "resultFile": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 154, + "startColumn": 37, + "endLine": 154, + "endColumn": 55 + } + } + } + ], + "relatedLocations": [ + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 167, + "startColumn": 21, + "endLine": 167, + "endColumn": 23 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 177, + "startColumn": 21, + "endLine": 177, + "endColumn": 23 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 192, + "startColumn": 21, + "endLine": 192, + "endColumn": 23 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 198, + "startColumn": 21, + "endLine": 198, + "endColumn": 23 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 204, + "startColumn": 21, + "endLine": 204, + "endColumn": 23 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 219, + "startColumn": 21, + "endLine": 219, + "endColumn": 28 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 229, + "startColumn": 25, + "endLine": 229, + "endColumn": 30 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 232, + "startColumn": 29, + "endLine": 232, + "endColumn": 32 + } + } + }, + { + "physicalLocation": { + "uri": "file:///workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs", + "region": { + "startLine": 254, + "startColumn": 17, + "endLine": 254, + "endColumn": 22 + } + } + } + ], + "properties": { + "warningLevel": 1, + "customProperties": { + "0": "+2 (incl 1 for nesting)", + "1": "+2 (incl 1 for nesting)", + "2": "+2 (incl 1 for nesting)", + "3": "+2 (incl 1 for nesting)", + "4": "+2 (incl 1 for nesting)", + "5": "+2 (incl 1 for nesting)", + "6": "+2 (incl 1 for nesting)", + "7": "+3 (incl 2 for nesting)", + "8": "+2 (incl 1 for nesting)" + } + } + }, + { + "ruleId": "ASP0025", + "level": "note", + "message": "Use AddAuthorizationBuilder to register authorization services and construct policies", + "locations": [ + { + "resultFile": { + "uri": "file:///workspaces/AS400API/Program.cs", + "region": { + "startLine": 100, + "startColumn": 1, + "endLine": 106, + "endColumn": 3 + } + } + } + ], + "properties": { + "warningLevel": 1 + } + }, + { + "ruleId": "CA1861", + "level": "note", + "message": "Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array", + "locations": [ + { + "resultFile": { + "uri": "file:///workspaces/AS400API/Infrastructure/DatabaseRowFormatter.cs", + "region": { + "startLine": 48, + "startColumn": 20, + "endLine": 48, + "endColumn": 43 + } + } + } + ], + "properties": { + "warningLevel": 1, + "customProperties": { + "paramName": "separator" + } + } + } + ], + "rules": { + "ASP0025": { + "id": "ASP0025", + "shortDescription": "Use AddAuthorizationBuilder", + "defaultLevel": "note", + "helpUri": "https://aka.ms/aspnet/analyzers", + "properties": { + "category": "Usage", + "isEnabledByDefault": true + } + }, + "CA1861": { + "id": "CA1861", + "shortDescription": "Avoid constant arrays as arguments", + "fullDescription": "Constant arrays passed as arguments are not reused when called repeatedly, which implies a new array is created each time. Consider extracting them to 'static readonly' fields to improve performance if the passed array is not mutated within the called method.", + "defaultLevel": "note", + "helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1861", + "properties": { + "category": "Performance", + "isEnabledByDefault": true, + "tags": [ + "Telemetry", + "EnabledRuleInAggressiveMode" + ] + } + }, + "S101": { + "id": "S101", + "shortDescription": "Types should be named in PascalCase", + "fullDescription": "Shared naming conventions allow teams to collaborate efficiently. This rule checks whether or not type names are using PascalCase. To reduce noise, two consecutive upper case characters are allowed unless they form the whole type name. So, MyXClass is compliant, but XC on its own is not.", + "defaultLevel": "warning", + "helpUri": "https://rules.sonarsource.com/csharp/RSPEC-101", + "properties": { + "category": "Minor Code Smell", + "isEnabledByDefault": true, + "tags": [ + "C#", + "MainSourceScope", + "TestSourceScope", + "SonarWay" + ] + } + }, + "S1144": { + "id": "S1144", + "shortDescription": "Unused private types or members should be removed", + "fullDescription": "private or internal types or private members that are never executed or referenced are dead code: unnecessary, inoperative code that should be removed. Cleaning out dead code decreases the size of the maintained codebase, making it easier to understand the program and preventing bugs from being introduced.", + "defaultLevel": "note", + "helpUri": "https://rules.sonarsource.com/csharp/RSPEC-1144", + "properties": { + "category": "Major Code Smell", + "isEnabledByDefault": true, + "tags": [ + "C#", + "MainSourceScope", + "TestSourceScope", + "SonarWay", + "Unnecessary" + ] + } + }, + "S125": { + "id": "S125", + "shortDescription": "Sections of code should not be commented out", + "fullDescription": "Programmers should not comment out code as it bloats programs and reduces readability.", + "defaultLevel": "warning", + "helpUri": "https://rules.sonarsource.com/csharp/RSPEC-125", + "properties": { + "category": "Major Code Smell", + "isEnabledByDefault": true, + "tags": [ + "C#", + "MainSourceScope", + "TestSourceScope", + "SonarWay" + ] + } + }, + "S2068": { + "id": "S2068", + "shortDescription": "Hard-coded credentials are security-sensitive", + "fullDescription": "Because it is easy to extract strings from an application source code or binary, credentials should not be hard-coded. This is particularly true for applications that are distributed or that are open-source.", + "defaultLevel": "warning", + "helpUri": "https://rules.sonarsource.com/csharp/RSPEC-2068", + "properties": { + "category": "Blocker Security Hotspot", + "isEnabledByDefault": true, + "tags": [ + "C#", + "MainSourceScope", + "SonarWay" + ] + } + }, + "S3776": { + "id": "S3776", + "shortDescription": "Cognitive Complexity of methods should not be too high", + "fullDescription": "Cognitive Complexity is a measure of how hard the control flow of a method is to understand. Methods with high Cognitive Complexity will be difficult to maintain.", + "defaultLevel": "warning", + "helpUri": "https://rules.sonarsource.com/csharp/RSPEC-3776", + "properties": { + "category": "Critical Code Smell", + "isEnabledByDefault": false, + "tags": [ + "C#", + "MainSourceScope", + "TestSourceScope", + "SonarWay" + ] + } + }, + "S6444": { + "id": "S6444", + "shortDescription": "Not specifying a timeout for regular expressions is security-sensitive", + "fullDescription": "Not specifying a timeout for regular expressions can lead to a Denial-of-Service attack. Pass a timeout when using System.Text.RegularExpressions to process untrusted input because a malicious user might craft a value for which the evaluation lasts excessively long.", + "defaultLevel": "warning", + "helpUri": "https://rules.sonarsource.com/csharp/RSPEC-6444", + "properties": { + "category": "Major Security Hotspot", + "isEnabledByDefault": true, + "tags": [ + "C#", + "MainSourceScope", + "SonarWay" + ] + } + }, + "SYSLIB1045": { + "id": "SYSLIB1045", + "shortDescription": "Convert to 'GeneratedRegexAttribute'.", + "defaultLevel": "note", + "helpUri": "https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1045", + "properties": { + "category": "Performance", + "isEnabledByDefault": true + } + } + } + } + ] +} \ No newline at end of file diff --git a/.sonarqube/out/0/ProjectInfo.xml b/.sonarqube/out/0/ProjectInfo.xml new file mode 100644 index 0000000..debfcca --- /dev/null +++ b/.sonarqube/out/0/ProjectInfo.xml @@ -0,0 +1,20 @@ + + + AS400API + C# + Product + 40cb5b53-77b8-b1fd-458c-805350cb09e8 + /workspaces/AS400API/AS400API.csproj + false + + + + + /workspaces/AS400API/.sonarqube/out/0/Issues.json + /workspaces/AS400API/.sonarqube/out/0 + /workspaces/AS400API/.sonarqube/out/0/Telemetry.json + + Debug + AnyCPU + net9.0 + \ No newline at end of file diff --git a/.sonarqube/out/0/Telemetry.json b/.sonarqube/out/0/Telemetry.json new file mode 100644 index 0000000..5d55dc1 --- /dev/null +++ b/.sonarqube/out/0/Telemetry.json @@ -0,0 +1 @@ +{"dotnetenterprise.s4net.build.target_framework_moniker":".NETCoreApp,Version=v9.0"} diff --git a/.sonarqube/out/0/output-cs/file-metadata.pb b/.sonarqube/out/0/output-cs/file-metadata.pb new file mode 100644 index 0000000..5d56cf3 --- /dev/null +++ b/.sonarqube/out/0/output-cs/file-metadata.pb @@ -0,0 +1,22 @@ +2 +)/workspaces/AS400API/Auth/AuthPolicies.csutf-8. +%/workspaces/AS400API/Auth/DemoUser.csutf-83 +*/workspaces/AS400API/Auth/DemoUserStore.csutf-82 +)/workspaces/AS400API/Auth/LoginRequest.csutf-83 +*/workspaces/AS400API/Auth/LoginResponse.csutf-84 ++/workspaces/AS400API/Auth/PasswordHasher.csutf-8+ +"/workspaces/AS400API/Auth/Roles.csutf-82 +)/workspaces/AS400API/Auth/TokenService.csutf-89 +0/workspaces/AS400API/Configuration/JwtOptions.csutf-8: +1/workspaces/AS400API/Configuration/OdbcOptions.csutf-89 +0/workspaces/AS400API/Endpoints/As400Endpoints.csutf-8? +6/workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.csutf-88 +//workspaces/AS400API/Endpoints/AuthEndpoints.csutf-8: +1/workspaces/AS400API/Endpoints/SystemEndpoints.csutf-8D +;/workspaces/AS400API/Infrastructure/DatabaseRowFormatter.csutf-8D +;/workspaces/AS400API/Infrastructure/DataReaderExtensions.csutf-8( +/workspaces/AS400API/Program.csutf-8K +@/workspaces/AS400API/obj/Debug/net9.0/AS400API.GlobalUsings.g.csutf-8_ +T/workspaces/AS400API/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.csutf-8I +>/workspaces/AS400API/obj/Debug/net9.0/AS400API.AssemblyInfo.csutf-8\ +Q/workspaces/AS400API/obj/Debug/net9.0/AS400API.MvcApplicationPartsAssemblyInfo.csutf-8 \ No newline at end of file diff --git a/.sonarqube/out/0/output-cs/log.pb b/.sonarqube/out/0/output-cs/log.pb new file mode 100644 index 0000000..6159c8d --- /dev/null +++ b/.sonarqube/out/0/output-cs/log.pb @@ -0,0 +1 @@ +Roslyn version: 4.14.0.0Language version: CSharp13!Concurrent execution: enabledgcFile '/workspaces/AS400API/obj/Debug/net9.0/AS400API.GlobalUsings.g.cs' was recognized as generated{wFile '/workspaces/AS400API/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs' was recognized as generatedeaFile '/workspaces/AS400API/obj/Debug/net9.0/AS400API.AssemblyInfo.cs' was recognized as generatedxtFile '/workspaces/AS400API/obj/Debug/net9.0/AS400API.MvcApplicationPartsAssemblyInfo.cs' was recognized as generated \ No newline at end of file diff --git a/.sonarqube/out/0/output-cs/metrics.pb b/.sonarqube/out/0/output-cs/metrics.pb new file mode 100644 index 0000000..5135524 --- /dev/null +++ b/.sonarqube/out/0/output-cs/metrics.pb @@ -0,0 +1,30 @@ +5 +)/workspaces/AS400API/Auth/AuthPolicies.csrH +%/workspaces/AS400API/Auth/DemoUser.cs 8r +  + ] +*/workspaces/AS400API/Auth/DemoUserStore.cs 8r  !"  1 +)/workspaces/AS400API/Auth/LoginRequest.csr3 +*/workspaces/AS400API/Auth/LoginResponse.csrX ++/workspaces/AS400API/Auth/PasswordHasher.cs 8r + . +"/workspaces/AS400API/Auth/Roles.csrn +)/workspaces/AS400API/Auth/TokenService.cs 8r(  "#$%'()*+,./0x "$()+.[ +0/workspaces/AS400API/Configuration/JwtOptions.cs 8 r + xp +1/workspaces/AS400API/Configuration/OdbcOptions.cs 8j r +  !"#$%&x  !"#$ +0/workspaces/AS400API/Endpoints/As400Endpoints.csD 8 jr +  !"#$%&'()*+,-/0123456789:;<=>?@ABDEFGHIJKLMNPQRSTUWXYZ[\]^_`abcefghijklmnoqrstuvwyz|}~xN !"&'*/146789=>ADFIJLMWXYZ^_begjkmnyz|}~ +6/workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs5 8jr +  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~xhz +//workspaces/AS400API/Endpoints/AuthEndpoints.cs 8 r*  !"#%&'()*+,./0x!"#%'().u +1/workspaces/AS400API/Endpoints/SystemEndpoints.cs 8r& + !"#$%&')*+x !%) +;/workspaces/AS400API/Infrastructure/DatabaseRowFormatter.cs 8rK +  !"#%&()*+,-/01245679:;<=>?ABDEFGHIJKLNOPQRSUVWx !"%*,/01469<>AEFIKNOQUo +;/workspaces/AS400API/Infrastructure/DataReaderExtensions.cs 8r + x + +  +/workspaces/AS400API/Program.csQ8j *rj  !#$%&'(+,-/012345689:;<=>?@ABCDEGHIJKLMNOPQSTUVWXYZ[\]^_`abdefghijlnopqrtvwy{|}~x;  !#$%'+,-/02<=BGHJMNOPQSTUWZ\`dfghilnpqtvwy{|}~ \ No newline at end of file diff --git a/.sonarqube/out/0/output-cs/symrefs.pb b/.sonarqube/out/0/output-cs/symrefs.pb new file mode 100644 index 0000000..8f59c53 --- /dev/null +++ b/.sonarqube/out/0/output-cs/symrefs.pb @@ -0,0 +1,448 @@ +O +)/workspaces/AS400API/Auth/AuthPolicies.cs + +  + + ' + + $ +%/workspaces/AS400API/Auth/DemoUser.cs + +  + #    +, 8 + + # +A M   # +k p    +     +  + +  +     +' ,    +*/workspaces/AS400API/Auth/DemoUserStore.cs + + ! +  2 8     + + / +7 ? # +- 1$ ( + + # +- 1( ,; ? +: B & + & " % +. 6   $ +? G7 ? +e j  2 7 +   & * +   , 0O +)/workspaces/AS400API/Auth/LoginRequest.cs + + ! + +) 1 + +: Bh +*/workspaces/AS400API/Auth/LoginResponse.cs + + " + +* 5 + +; D + +M V + +t y ++/workspaces/AS400API/Auth/PasswordHasher.cs + + " +   2 : + + + f mo v +   Z dc m + +  - 9 +  A I- 5 + 7 ;J N + ' 0 + +  +% -1 9 +6 @3 = +I S1 ; + ; D + D Q + 7 BH +"/workspaces/AS400API/Auth/Roles.cs + +  + +  + +  +)/workspaces/AS400API/Auth/TokenService.cs + +   < +   ( < D(( )) ++0 8 +- :..  +# *  + + ( +' +- 1! %"" ! + ,, 2 + $$ **  +'' ..( - +0/workspaces/AS400API/Configuration/JwtOptions.cs + +  + + # + + + +  + +    +   & )I L +   ) & + +  +1/workspaces/AS400API/Configuration/OdbcOptions.cs + +  +   +   #' 7X h + + + ' +? C +   ' /C K +   ' -  D J + !!' 0!!J S + ""' 3""Q ] + ##  + + $ +  "Z + : ?. 32 7  0 5!!3 8""6 ;## $$ % + +0/workspaces/AS400API/Endpoints/As400Endpoints.cs + + " + +$ 5> +M R // DD ee  + ( +6 9. 1Y \2 5 +J N " & +_ l &2 +   !! &&  +    $ +   !!O X""" + +//B F44 66% ) +//W d11 & +11 88 ==  +55 %665 = +66 77 ' +77 88H Q99" +2 +DD7 BJJ. 9WWD OXXk v^^Z e +DDS WII WW$ ( +DDh uFF &( +FF LL XX ^^  +PP %WW4 < +WW XX] cYY & +YY ZZ" +6 +ee@ Kkk. 9}}) 4 +f q +T _8 +eeT ]kkX a +' 0 +s | +a j +een rjj yy* . + +ee gg &, +gg mm  +  +  +qq %zz& .L +yy 'zz ||' .~~  +% , +  +0 7 +|| $}} ~~' 7$ + + " +  +' 50 + +  +  +W ^ + '< + +! ' +! ' +, 2 + & +5 ;$ + +  +  +$ '< + +! " +( ) +? @ +/ 0 +O P + +  +" + + +/ 3 +7 ; +6/workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs + + " +% 0 += H + +" 4 + +$ 6$ + +N S +  +  + ++ 0 + $ +X ] +# ( +3 8 +, 1 +3 8 +, 1 +3 8 +, 1 +  +  + $ + + # +  +. 2 + + , + *0 + +  +  +  + < + +  +  +  +, 0 + H + +  +  +* 2 +8 @ +( 0 + $ + + " + " +& 0 + +  +( . + + " +  +  +  +  + " + " + " +  +  +* 4 + +P V +a gT + + ' + ' + ' + ' + # + # +* 9< + +$ + +  +( / +  +4 ;$ + + % + ! +/ 80 + +  +  +  +$ (< + +% + +% + +0 6 +, 2 +$ *$ + + # + # +% (< + +% & +, - +C D +F G +3 4 + +$ ) +9 > + + ! +& / + +0 4 +8 < + + # + *0 + + ! +9 C +2 < + (0 + + " +9 D +2 = + )0 + +  +9 B +2 ; + '$ + +  +% ) +^ b + +  +) 1 +//workspaces/AS400API/Endpoints/AuthEndpoints.cs + +   ! + +  $ 4( +  L Q %% .. < +J Q = Dl s7 >E L +a j &! * + y  * + + = G2 +  ? C7 ;s w + 0 ;( +%%6 :'' ''2 6((  +'' ))$ , +(( )). 3 +((= >((B C +1/workspaces/AS400API/Endpoints/SystemEndpoints.cs + +   # + +  ( 8 +  T W     + +$ 6 +N S ))  +9 =  $ +  ) , +   " + S T +;/workspaces/AS400API/Infrastructure/DatabaseRowFormatter.cs + + ( + + + +< S + + +n r    +3 7 ! + +/ D +f l " + "" %%  +!! ""  +(( %!!E P2 +((- 2**! &,, // 66 F +// 44 99 ;; "EE FF $HH " +11 11 2 +;; << >> AA) 0AA7 >2 +DD EE +NN QQ UU ( +FF FF FF- .HH# $2 +HH II NN, 3OO QQ ! +;/workspaces/AS400API/Infrastructure/DataReaderExtensions.cs + + ( + + ,( +? E + +   %( +K R + + #$ +6 = +    + ' "( + & 2   +/workspaces/AS400API/Program.cs +     + 6v +     ++ // 00 MM NN OO PP QQ SS dd ll +  +  ' + +" * +, ? n + . 2     !! ## "$$ "%% "MM "NNC G: +++ ,, +--A KOO (ZZ $\\ & +-- ^^ )( +00 &22 GG HH ( +88 GG" 0GG? MJJ +  + +NN+ , +UU WW  +dd" )ff hh  +ff4 :gg  +hh1 7ii \ +ll nn pp qq tt vv ww yy {{ +  * +{{ || }} ~~   \ No newline at end of file diff --git a/.sonarqube/out/0/output-cs/token-cpd.pb b/.sonarqube/out/0/output-cs/token-cpd.pb new file mode 100644 index 0000000..c17bae0 --- /dev/null +++ b/.sonarqube/out/0/output-cs/token-cpd.pb @@ -0,0 +1,5928 @@ + +)/workspaces/AS400API/Auth/AuthPolicies.cs + namespace  +AS400API +  +.  +Auth  +;  +public  +static  +class  + AuthPolicies  +{  +public + +const  +string  +RequireOperator ' +=( ) +$str* ; +;; < +public + +const  +string  + RequireAdmin $ +=% & +$str' 5 +;5 6 +}  +%/workspaces/AS400API/Auth/DemoUser.cs + namespace  +AS400API +  +.  +Auth  +;  +public  +sealed  +class  +DemoUser  +{  +public + +DemoUser  +(  +string  +username # +,# $ +string% + + passwordHash, 8 +,8 9 +string: @ + passwordSaltA M +,M N +IReadOnlyCollectionO b +<b c +stringc i +>i j +rolesk p +)p q +{  +Username    +=    +username    +;    + PasswordHash + +  += + +  + passwordHash + + # +; + +# $ + PasswordSalt    +=    + passwordSalt   # +;  # $ +Roles    +=    +roles    +;    +}    +public + +string  +Username  +{  +get  +; ! +}" # +public + +string  + PasswordHash  +{  +get! $ +;$ % +}& ' +public + +string  + PasswordSalt  +{  +get! $ +;$ % +}& ' +public + +IReadOnlyCollection  +<  +string % +>% & +Roles' , +{- . +get/ 2 +;2 3 +}4 5 +}  +*/workspaces/AS400API/Auth/DemoUserStore.cs + namespace  +AS400API +  +.  +Auth  +;  +public  +sealed  +class  + DemoUserStore ! +{  +private    +readonly    + +Dictionary    +<    +string   & +,  & ' +DemoUser  ( 0 +>  0 1 +_users  2 8 +;  8 9 +public   + + DemoUserStore    +(    +)    +{    +_users    +=    +new    + +Dictionary    +<    +string   & +,  & ' +DemoUser  ( 0 +>  0 1 +(  1 2 +StringComparer  2 @ +.  @ A +OrdinalIgnoreCase  A R +)  R S +{  +[  +$str  +]  +=  + +CreateUser " +(" # +$str# * +,* + +$str, 6 +,6 7 +new8 ; +[; < +]< = +{> ? +Roles@ E +.E F +AdminF K +,K L +RolesM R +.R S +OperatorS [ +}\ ] +)] ^ +,^ _ +[  +$str  +]  +=  + +CreateUser % +(% & +$str& 0 +,0 1 +$str2 < +,< = +new> A +[A B +]B C +{D E +RolesF K +.K L +OperatorL T +}U V +)V W +}  +; + +}  +public + + ValueTask  +<  +DemoUser  +?  +>  +FindByNameAsync / +(/ 0 +string0 6 +username7 ? +)? @ +{  +_users  +.  + TryGetValue  +(  +username # +,# $ +out% ( +var) , +user- 1 +)1 2 +;2 3 +return  + ValueTask  +.  + +FromResult # +(# $ +user$ ( +)( ) +;) * +}  +public + +bool  +ValidateCredentials # +(# $ +DemoUser$ , +user- 1 +,1 2 +string3 9 +password: B +)B C +=>D F +PasswordHasher  +.  +Verify  +(  +password & +,& ' +user( , +., - + PasswordHash- 9 +,9 : +user; ? +.? @ + PasswordSalt@ L +)L M +;M N +private  +static  +DemoUser  + +CreateUser & +(& ' +string' - +username. 6 +,6 7 +string8 > +password? G +,G H +IReadOnlyCollectionI \ +<\ ] +string] c +>c d +rolese j +)j k +{  +var  +(  +hash  +,  +salt  +)  +=  +PasswordHasher ) +.) * + HashPassword* 6 +(6 7 +password7 ? +)? @ +;@ A +return    +new    +DemoUser    +(    +username   $ +,  $ % +hash  & * +,  * + +salt  , 0 +,  0 1 +roles  2 7 +)  7 8 +;  8 9 +}!!  +}""  +)/workspaces/AS400API/Auth/LoginRequest.cs + namespace  +AS400API +  +.  +Auth  +;  +public  +sealed  +record  + LoginRequest ! +(! " +string" ( +Username) 1 +,1 2 +string3 9 +Password: B +)B C +;C D +*/workspaces/AS400API/Auth/LoginResponse.cs + namespace  +AS400API +  +.  +Auth  +;  +public  +sealed  +record  + LoginResponse " +(" # +string# ) + AccessToken* 5 +,5 6 +int7 : + ExpiresIn; D +,D E +stringF L + TokenTypeM V +,V W +IReadOnlyCollectionX k +<k l +stringl r +>r s +Rolest y +)y z +;z { ++/workspaces/AS400API/Auth/PasswordHasher.cs + namespace  +AS400API +  +.  +Auth  +;  +public  +static  +class  +PasswordHasher " +{  +private    +const    +int    +SaltSize    +=    +$num  ! # +;  # $ +private + +  +const + +  +int + +  +KeySize + +  += + +  +$num + + " +; + +" # +private    +const    +int    + +Iterations    +=  ! " +$num  # * +;  * + +public   + +static    +(    +string    +Hash    +,    +string   & +Salt  ' + +)  + , + HashPassword  - 9 +(  9 : +string  : @ +password  A I +)  I J +{  +var  +salt  += ! +RandomNumberGenerator ( +.( ) +GetBytes) 1 +(1 2 +SaltSize2 : +): ; +;; < +var  + hashBytes  +=  + KeyDerivation % +.% & +Pbkdf2& , +(, - +password- 5 +,5 6 +salt7 ; +,; < +KeyDerivationPrf= M +.M N + +HMACSHA256N X +,X Y + +IterationsZ d +,d e +KeySizef m +)m n +;n o +return  +(  +Convert  +.  +ToBase64String & +(& ' + hashBytes' 0 +)0 1 +,1 2 +Convert3 : +.: ; +ToBase64String; I +(I J +saltJ N +)N O +)O P +;P Q +}  +public + +static  +bool  +Verify  +(  +string $ +password% - +,- . +string/ 5 + +storedHash6 @ +,@ A +stringB H + +storedSaltI S +)S T +{  +var  + saltBytes  +=  +Convert  +.  +FromBase64String 0 +(0 1 + +storedSalt1 ; +); < +;< = +var  + computedBytes  +=  + KeyDerivation ) +.) * +Pbkdf2* 0 +(0 1 +password1 9 +,9 : + saltBytes; D +,D E +KeyDerivationPrfF V +.V W + +HMACSHA256W a +,a b + +Iterationsc m +,m n +KeySizeo v +)v w +;w x +var  + storedBytes  +=  +Convert ! +.! " +FromBase64String" 2 +(2 3 + +storedHash3 = +)= > +;> ? +return # +CryptographicOperations & +.& ' +FixedTimeEquals' 6 +(6 7 + storedBytes7 B +,B C + computedBytesD Q +)Q R +;R S +}  +}  +"/workspaces/AS400API/Auth/Roles.cs + namespace  +AS400API +  +.  +Auth  +;  +public  +static  +class  +Roles  +{  +public + +const  +string  +Admin  +=  +$str ' +;' ( +public + +const  +string  +Operator  +=! " +$str# - +;- . +}  +)/workspaces/AS400API/Auth/TokenService.cs + namespace   +AS400API   +  +.    +Auth    +;    +public   +sealed    +class    + TokenService    +{   +private    +readonly    + +JwtOptions    +_options   ( +;  ( ) +private  +readonly # +JwtSecurityTokenHandler , + _tokenHandler- : +=; < +new= @ +(@ A +)A B +;B C +public + + TokenService  +(  + +JwtOptions " +options# * +)* + +{  +_options  +=  +options  +;  +}  +public + +string  + CreateToken  +(  +DemoUser & +user' + +)+ , +{  +var  +signingCredentials  +=  +new! $ +SigningCredentials% 7 +(7 8 +new  +SymmetricSecurityKey $ +($ % +Encoding% - +.- . +UTF8. 2 +.2 3 +GetBytes3 ; +(; < +_options< D +.D E +KeyE H +)H I +)I J +,J K +SecurityAlgorithms  +.  + +HmacSha256 ) +)) * +;* + +var  +claims  +=  +new  +List  +<  +Claim # +># $ +{  +new  +( # +JwtRegisteredClaimNames ' +.' ( +Sub( + +,+ , +user- 1 +.1 2 +Username2 : +): ; +,; < +new  +( # +JwtRegisteredClaimNames ' +.' ( +Jti( + +,+ , +Guid- 1 +.1 2 +NewGuid2 9 +(9 : +): ; +.; < +ToString< D +(D E +)E F +)F G +,G H +new  +(  + +ClaimTypes  +.  +Name  +,  +user! % +.% & +Username& . +). / +}    +;   + +foreach""  +(""  +var""  +role""  +in""  +user"" ! +.""! " +Roles""" ' +)""' ( +{##  +claims$$  +.$$  +Add$$  +($$  +new$$  +Claim$$  +($$ ! + +ClaimTypes$$! + +.$$+ , +Role$$, 0 +,$$0 1 +role$$2 6 +)$$6 7 +)$$7 8 +;$$8 9 +}%%  +var''  +token''  +=''  +new''  +JwtSecurityToken'' ( +(''( ) +issuer((  +:((  +_options((  +.((  +Issuer(( # +,((# $ +audience))  +:))  +_options))  +.))  +Audience)) ' +,))' ( +claims**  +:**  +claims**  +,**  +expires++  +:++  +DateTime++  +.++  +UtcNow++ $ +.++$ % + +AddMinutes++% / +(++/ 0 +_options++0 8 +.++8 9& +AccessTokenLifetimeMinutes++9 S +)++S T +,++T U +signingCredentials,,  +:,,  +signingCredentials,, 2 +),,2 3 +;,,3 4 +return..  + _tokenHandler..  +...  + +WriteToken.. ' +(..' ( +token..( - +)..- . +;... / +}//  +}00  +0/workspaces/AS400API/Configuration/JwtOptions.cs + namespace  +AS400API +  +.  + Configuration  +; ! +public  +sealed  +class  + +JwtOptions  +{  +public + +const  +string  + SectionName # +=$ % +$str& + +;+ , +public + + + +string + +  +Issuer + +  +{ + +  +get + +  +; + +  +set + + # +; + +# $ +} + +% & += + +' ( +$str + +) 3 +; + +3 4 +public   + +string    +Audience    +{    +get    +;   ! +set  " % +;  % & +}  ' ( +=  ) * +$str  + = +;  = > +public   + +string    +Key    +{    +get    +;    +set    +;   ! +}  " # +=  $ % +$str  & I +;  I J +public   + +int   & +AccessTokenLifetimeMinutes   ) +{  * + +get  , / +;  / 0 +set  1 4 +;  4 5 +}  6 7 +=  8 9 +$num  : < +;  < = +public + +void  + EnsureIsValid  +(  +)  +{  +if + +(  +string  +.  +IsNullOrWhiteSpace % +(% & +Key& ) +)) * +||+ - +Encoding. 6 +.6 7 +UTF87 ; +.; < + GetByteCount< H +(H I +KeyI L +)L M +<N O +$numP R +)R S +{  +throw  +new % +InvalidOperationException / +(/ 0 +$str0  +)   +; +  +}  +if + +( & +AccessTokenLifetimeMinutes & +<=' ) +$num* + +)+ , +{  +throw  +new % +InvalidOperationException / +(/ 0 +$str0 k +)k l +;l m +}  +}  +} ( +1/workspaces/AS400API/Configuration/OdbcOptions.cs + namespace  +AS400API +  +.  + Configuration  +; ! +public  +sealed  +class  + OdbcOptions  +{  +public + +string  +?  +System  +{  +get  +;  +set! $ +;$ % +}& ' +public   + +string    +?    +DefaultLibraries   # +{  $ % +get  & ) +;  ) * +set  + . +;  . / +}  0 1 +public + + + +string + +  +? + +  +User + +  +{ + +  +get + +  +; + +  +set + + " +; + +" # +} + +$ % +public   + +string    +?    +Password    +{    +get   ! +;  ! " +set  # & +;  & ' +}  ( ) +public + +string  +?  +Naming  +{  +get  +;  +set! $ +;$ % +}& ' +public + +string  +?  + Translate  +{  +get " +;" # +set$ ' +;' ( +}) * +=+ , +$str- 0 +;0 1 +public + +string  +?  + ClientLocale  +{ ! +get" % +;% & +set' * +;* + +}, - +=. / +$str0 7 +;7 8 +public + +bool  +Pooling  +{  +get  +;  +set " +;" # +}$ % +=& ' +true( , +;, - +public + +string  +ToConnectionString $ +($ % +)% & +{  +var  + +driverName  +=  + Environment $ +.$ %" +GetEnvironmentVariable% ; +(; < +$str< O +)O P +??Q S +$strT n +;n o +var  +parts  +=  +new  +List  +<  +string # +># $ +{  +$"  +$str  +{  + +driverName " +}" # +$str# % +"% & +,& ' +$"  +$str  +{  +System  +}  +"  +}  +; + +if + +(  +!  +string  +.  +IsNullOrWhiteSpace & +(& ' +DefaultLibraries' 7 +)7 8 +)8 9 +parts: ? +.? @ +Add@ C +(C D +$"D F +$strF W +{W X +DefaultLibrariesX h +}h i +"i j +)j k +;k l +if + +(  +!  +string  +.  +IsNullOrWhiteSpace & +(& ' +User' + +)+ , +), - +parts. 3 +.3 4 +Add4 7 +(7 8 +$"8 : +$str: > +{> ? +User? C +}C D +"D E +)E F +;F G +if + +(  +!  +string  +.  +IsNullOrWhiteSpace & +(& ' +Password' / +)/ 0 +)0 1 +parts2 7 +.7 8 +Add8 ; +(; < +$"< > +$str> B +{B C +PasswordC K +}K L +"L M +)M N +;N O +if   + +(    +!    +string    +.    +IsNullOrWhiteSpace   & +(  & ' +Naming  ' - +)  - . +)  . / +parts  0 5 +.  5 6 +Add  6 9 +(  9 : +$"  : < +$str  < C +{  C D +Naming  D J +}  J K +"  K L +)  L M +;  M N +if!! + +(!!  +!!!  +string!!  +.!!  +IsNullOrWhiteSpace!! & +(!!& ' + Translate!!' 0 +)!!0 1 +)!!1 2 +parts!!3 8 +.!!8 9 +Add!!9 < +(!!< = +$"!!= ? +$str!!? I +{!!I J + Translate!!J S +}!!S T +"!!T U +)!!U V +;!!V W +if"" + +(""  +!""  +string""  +.""  +IsNullOrWhiteSpace"" & +(""& ' + ClientLocale""' 3 +)""3 4 +)""4 5 +parts""6 ; +.""; < +Add""< ? +(""? @ +$"""@ B +$str""B P +{""P Q + ClientLocale""Q ] +}""] ^ +"""^ _ +)""_ ` +;""` a +if## + +(##  +Pooling##  +)##  +parts##  +.##  +Add##  +(##  +$str## - +)##- . +;##. / +return$$  +string$$  +.$$  +Join$$  +($$  +$str$$  +,$$  +parts$$ % +)$$% & +;$$& ' +}%%  +}&& } +0/workspaces/AS400API/Endpoints/As400Endpoints.cs + namespace   +AS400API   +  +.    + Endpoints    +;    +public  +static  +class  +As400Endpoints " +{  +public + +static  +RouteGroupBuilder # +MapAs400Endpoints$ 5 +(5 6 +this6 : +RouteGroupBuilder; L +groupM R +)R S +{  +group  +.  +MapGet  +(  +$str & +,& ' +async( - +(. / +string/ 5 +sql6 9 +,9 : +OdbcConnection; I +connJ N +,N O +ILoggerFactoryP ^ + loggerFactory_ l +)l m +=>n p +{  +var  +logger  +=  + loggerFactory & +.& ' + CreateLogger' 3 +(3 4 +$str4 H +)H I +;I J +try  +{  +await  +conn  +.  + OpenAsync $ +($ % +)% & +;& ' +if  +(  +string  +.  +IsNullOrWhiteSpace - +(- . +sql. 1 +)1 2 +)2 3 +{  +logger  +.  + +LogWarning % +(% & +$str& _ +)_ ` +;` a +return  +Results " +." # + +BadRequest# - +(- . +new. 1 +{2 3 +error4 9 +=: ; +$str< ` +}a b +)b c +;c d +}  +logger  +.  +LogInformation % +(% & +$str& W +,W X +sqlY \ +?\ ] +.] ^ +Length^ d +??e g +$numh i +)i j +;j k +var  +rows  +=  +(  +await ! +conn" & +.& ' + +QueryAsync' 1 +(1 2 +sql2 5 +!5 6 +)6 7 +)7 8 +.8 9 +ToList9 ? +(? @ +)@ A +;A B +var    + formatted    +=    +rows   $ +.  $ %# +ToCamelCaseDictionaries  % < +(  < = +)  = > +.  > ? +ToList  ? E +(  E F +)  F G +;  G H +logger!!  +.!!  +LogInformation!! % +(!!% & +$str!!& M +,!!M N + formatted!!O X +.!!X Y +Count!!Y ^ +)!!^ _ +;!!_ ` +return""  +Results""  +.""  +Ok"" ! +(""! " + formatted""" + +)""+ , +;"", - +}##  +catch$$  +($$  + Exception$$  +ex$$  +)$$  +{%%  +logger&&  +.&&  +LogError&&  +(&&  +ex&& " +,&&" # +$str&&$ 9 +)&&9 : +;&&: ; +return''  +Results''  +.''  +Problem'' & +(''& ' +$"''' ) +$str'') 7 +{''7 8 +ex''8 : +.'': ; +Message''; B +}''B C +"''C D +)''D E +;''E F +}((  +}))  +))) + +.**  +RequireAuthorization**  +(**  + AuthPolicies** * +.*** + + RequireAdmin**+ 7 +)**7 8 +.++  + WithSummary++  +(++  +$str++ Q +)++Q R +.,,  +Produces,,  +(,,  +$num,,  +),,  +.--  +ProducesProblem--  +(--  +$num--  +)--  +;--  +group//  +.//  +MapGet//  +(//  +$str// * +,//* + +async//, 1 +(//2 3 +OdbcConnection//3 A +conn//B F +,//F G +ILoggerFactory//H V + loggerFactory//W d +)//d e +=>//f h +{00  +var11  +logger11  +=11  + loggerFactory11 & +.11& ' + CreateLogger11' 3 +(113 4 +$str114 L +)11L M +;11M N +try22  +{33  +await44  +conn44  +.44  + OpenAsync44 $ +(44$ % +)44% & +;44& ' +const55  +string55  +sqlQuery55 % +=55& ' +$str55( x +;55x y +var66  +schemas66  +=66  +(66  +await66 $ +conn66% ) +.66) * + +QueryAsync66* 4 +(664 5 +sqlQuery665 = +)66= > +)66> ? +.66? @ +ToList66@ F +(66F G +)66G H +;66H I +var77  + formatted77  +=77  +schemas77 ' +.77' (# +ToCamelCaseDictionaries77( ? +(77? @ +)77@ A +.77A B +ToList77B H +(77H I +)77I J +;77J K +logger88  +.88  +LogInformation88 % +(88% & +$str88& F +,88F G + formatted88H Q +.88Q R +Count88R W +)88W X +;88X Y +return99  +Results99  +.99  +Ok99 ! +(99! " + formatted99" + +)99+ , +;99, - +}::  +catch;;  +(;;  + Exception;;  +ex;;  +);;  +{<<  +logger==  +.==  +LogError==  +(==  +ex== " +,==" # +$str==$ C +)==C D +;==D E +return>>  +Results>>  +.>>  +Problem>> & +(>>& ' +$">>' ) +$str>>) A +{>>A B +ex>>B D +.>>D E +Message>>E L +}>>L M +">>M N +)>>N O +;>>O P +}??  +}@@  +)@@ + +.AA  +RequireAuthorizationAA  +(AA  + AuthPoliciesAA * +.AA* + +RequireOperatorAA+ : +)AA: ; +.BB  + WithSummaryBB  +(BB  +$strBB K +)BBK L +;BBL M +groupDD  +.DD  +MapGetDD  +(DD  +$strDD ' +,DD' ( +asyncDD) . +(DD/ 0 +stringDD0 6 + libraryNameDD7 B +,DDB C +OdbcConnectionDDD R +connDDS W +,DDW X +ILoggerFactoryDDY g + loggerFactoryDDh u +)DDu v +=>DDw y +{EE  +varFF  +loggerFF  +=FF  + loggerFactoryFF & +.FF& ' + CreateLoggerFF' 3 +(FF3 4 +$strFF4 I +)FFI J +;FFJ K +tryGG  +{HH  +awaitII  +connII  +.II  + OpenAsyncII $ +(II$ % +)II% & +;II& ' +ifJJ  +(JJ  +stringJJ  +.JJ  +IsNullOrWhiteSpaceJJ - +(JJ- . + libraryNameJJ. 9 +)JJ9 : +)JJ: ; +{KK  +loggerLL  +.LL  + +LogWarningLL % +(LL% & +$strLL& _ +)LL_ ` +;LL` a +returnMM  +ResultsMM " +.MM" # + +BadRequestMM# - +(MM- . +newMM. 1 +{MM2 3 +errorMM4 9 +=MM: ; +$strMM< h +}MMi j +)MMj k +;MMk l +}NN  +constPP  +stringPP  +sqlQueryPP % +=PP& ' +$strPU(  +;UU  +varWW  +tablesWW  +=WW  +(WW  +awaitWW # +connWW$ ( +.WW( ) + +QueryAsyncWW) 3 +(WW3 4 +sqlQueryWW4 < +,WW< = +newWW> A +{WWB C + libraryNameWWD O +}WWP Q +)WWQ R +)WWR S +.WWS T +ToListWWT Z +(WWZ [ +)WW[ \ +;WW\ ] +loggerXX  +.XX  +LogInformationXX % +(XX% & +$strXX& [ +,XX[ \ +tablesXX] c +.XXc d +CountXXd i +,XXi j + libraryNameXXk v +)XXv w +;XXw x +varYY  + formattedYY  +=YY  +tablesYY & +.YY& '# +ToCamelCaseDictionariesYY' > +(YY> ? +)YY? @ +.YY@ A +ToListYYA G +(YYG H +)YYH I +;YYI J +returnZZ  +ResultsZZ  +.ZZ  +OkZZ ! +(ZZ! " + formattedZZ" + +)ZZ+ , +;ZZ, - +}[[  +catch\\  +(\\  + Exception\\  +ex\\  +)\\  +{]]  +logger^^  +.^^  +LogError^^  +(^^  +ex^^ " +,^^" # +$str^^$ X +,^^X Y + libraryName^^Z e +)^^e f +;^^f g +return__  +Results__  +.__  +Problem__ & +(__& ' +$"__' ) +$str__) @ +{__@ A +ex__A C +.__C D +Message__D K +}__K L +"__L M +)__M N +;__N O +}``  +}aa  +)aa + +.bb  +RequireAuthorizationbb  +(bb  + AuthPoliciesbb * +.bb* + +RequireOperatorbb+ : +)bb: ; +.cc  + WithSummarycc  +(cc  +$strcc I +)ccI J +;ccJ K +groupee  +.ee  +MapGetee  +(ee  +$stree 0 +,ee0 1 +asyncee2 7 +(ee8 9 +stringee9 ? + libraryNameee@ K +,eeK L +stringeeM S + tableNameeeT ] +,ee] ^ +OdbcConnectionee_ m +conneen r +,eer s +ILoggerFactory eet  + loggerFactory +ee  +) +ee  +=> +ee  +{ff  +vargg  +loggergg  +=gg  + loggerFactorygg & +.gg& ' + CreateLoggergg' 3 +(gg3 4 +$strgg4 Q +)ggQ R +;ggR S +tryhh  +{ii  +awaitjj  +connjj  +.jj  + OpenAsyncjj $ +(jj$ % +)jj% & +;jj& ' +ifkk  +(kk  +stringkk  +.kk  +IsNullOrWhiteSpacekk - +(kk- . + libraryNamekk. 9 +)kk9 : +||kk; = +stringkk> D +.kkD E +IsNullOrWhiteSpacekkE W +(kkW X + tableNamekkX a +)kka b +)kkb c +{ll  +loggermm  +.mm  + +LogWarningmm % +(mm% & +$strmm& x +)mmx y +;mmy z +returnnn  +Resultsnn " +.nn" # + +BadRequestnn# - +(nn- . +newnn. 1 +{nn2 3 +errornn4 9 +=nn: ; +$strnn< z +}nn{ | +)nn| } +;nn} ~ +}oo  +constqq  +stringqq  +sqlQueryqq % +=qq& ' +$strqw(  +;ww  +awaityy  +usingyy  +varyy  +commandyy ' +=yy( ) +connyy* . +.yy. / + CreateCommandyy/ < +(yy< = +)yy= > +;yy> ? +commandzz  +.zz  + CommandTextzz # +=zz$ % +sqlQueryzz& . +;zz. / +var||  +libraryParameter|| $ +=||% & +command||' . +.||. / +CreateParameter||/ > +(||> ? +)||? @ +;||@ A +libraryParameter}}  +.}} ! +Value}}! & +=}}' ( + libraryName}}) 4 +.}}4 5 +ToUpper}}5 < +(}}< = +)}}= > +;}}> ? +command~~  +.~~  + +Parameters~~ " +.~~" # +Add~~# & +(~~& ' +libraryParameter~~' 7 +)~~7 8 +;~~8 9 +var +  +tableParameter + " += +# $ +command +% , +. +, - +CreateParameter +- < +( +< = +) += > +; +> ? +tableParameter +  +. +  +Value + $ += +% & + tableName +' 0 +. +0 1 +ToUpper +1 8 +( +8 9 +) +9 : +; +: ; +command +  +. +  + +Parameters + " +. +" # +Add +# & +( +& ' +tableParameter +' 5 +) +5 6 +; +6 7 +var +  +columns +  += +  +new + ! +List +" & +< +& ' + IDictionary +' 2 +< +2 3 +string +3 9 +, +9 : +object +; A +> +A B +> +B C +( +C D +) +D E +; +E F +await +  +using +  +( +  +var +  +reader +! ' += +( ) +await +* / +command +0 7 +. +7 8 +ExecuteReaderAsync +8 J +( +J K +) +K L +) +L M +{ +  +while +  +( +  +await +  +reader +! ' +. +' ( + ReadAsync +( 1 +( +1 2 +) +2 3 +) +3 4 +{ +  +var +  +row +  += + ! +new +" % + +Dictionary +& 0 +< +0 1 +string +1 7 +, +7 8 +object +9 ? +> +? @ +( +@ A +StringComparer +A O +. +O P +OrdinalIgnoreCase +P a +) +a b +; +b c +for +  +( +  +var +  +i +! " += +# $ +$num +% & +; +& ' +i +( ) +< +* + +reader +, 2 +. +2 3 + +FieldCount +3 = +; += > +i +? @ +++ +@ B +) +B C +{ +  +row +  +[ +  +reader + & +. +& ' +GetName +' . +( +. / +i +/ 0 +) +0 1 +] +1 2 += +3 4 +reader +5 ; +. +; < +GetNormalizedValue +< N +( +N O +i +O P +) +P Q +! +Q R +; +R S +} +  +columns +  +. +  +Add + # +( +# $ +row +$ ' +) +' ( +; +( ) +} +  +} +  +logger +  +. +  +LogInformation + % +( +% & +$str +& U +, +U V +columns +W ^ +. +^ _ +Count +_ d +, +d e + libraryName +f q +, +q r + tableName +s | +) +| } +; +} ~ +var +  + formatted +  += +  +columns + ' +. +' ( +Select +( . +( +. / +dict +/ 3 +=> +4 6 +dict +7 ; +. +; <# +ToCamelCaseDictionary +< Q +( +Q R +) +R S +) +S T +. +T U +ToList +U [ +( +[ \ +) +\ ] +; +] ^ +return +  +Results +  +. +  +Ok + ! +( +! " + formatted +" + +) ++ , +; +, - +} +  +catch +  +( +  + Exception +  +ex +  +) +  +{ +  +logger +  +. +  +LogError +  +( +  +ex + " +, +" # +$str +$ R +, +R S + libraryName +T _ +, +_ ` + tableName +a j +) +j k +; +k l +return +  +Results +  +. +  +Problem + & +( +& ' +$" +' ) +$str +) A +{ +A B +ex +B D +. +D E +Message +E L +} +L M +" +M N +) +N O +; +O P +} +  +} +  +) + + +. +  + WithSummary +  +( +  +$str + X +) +X Y +; +Y Z +return +  +group +  +; +  +} +  +}  +6/workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs + namespace   +AS400API   +  +.    + Endpoints    +;    +public  +static  +class  +ORDUAGEndpoint " +{  +private  +static  +readonly  +string " +[" # +]# $ + ColumnNames% 0 +=1 2 +[  +$str  +,  +$str  +,  +$str  +,  +$str  +,  +$str  +,  +$str  +,  +$str  +,  +$str  +,  +$str  +,  +$str  +,  +$str  +,  +$str  +,  +$str  +,  +$str    +,    +$str!!  +,!!  +$str""  +,""  +$str##  +,##  +$str$$  +,$$  +$str%%  +,%%  +$str&&  +,&&  +$str''  +,''  +$str((  +,((  +$str))  +,))  +$str**  +,**  +$str++  +,++  +$str,,  +,,,  +$str--  +,--  +$str..  +,..  +$str//  +,//  +$str00  +,00  +$str11  +,11  +$str22 # +,22# $ +$str33  +,33 ! +$str44  +,44  +$str55  +,55  +$str66  +,66  +$str77  +,77  +$str88  +,88  +$str99  +,99  +$str::  +,::  +$str;;  +,;;  +$str<< # +,<<# $ +$str==  +,==  +$str>>  +,>>  +$str??  +,??  +$str@@  +,@@  +$strAA  +,AA  +$strBB  +,BB  +$strCC  +,CC  +$strDD  +,DD  +$strEE  +,EE  +$strFF  +,FF  +$strGG  +,GG  +$strHH  +,HH  +$strII  +,II  +$strJJ  +,JJ  +$strKK " +,KK" # +$strLL " +,LL" # +$strMM " +,MM" # +$strNN " +,NN" # +$strOO " +,OO" # +$strPP " +,PP" # +$strQQ " +,QQ" # +$strRR " +,RR" # +$strSS " +,SS" # +$strTT # +,TT# $ +$strUU # +,UU# $ +$strVV # +,VV# $ +$strWW " +,WW" # +$strXX  +,XX  +$strYY $ +,YY$ % +$strZZ ! +,ZZ! " +$str[[ $ +,[[$ % +$str\\ ! +,\\! " +$str]] " +,]]" # +$str^^  +,^^  +$str__ ! +,__! " +$str``  +,``  +$straa ! +,aa! " +$strbb  +,bb  +$strcc  +,cc  +$strdd  +,dd  +$stree $ +,ee$ % +$strff ! +,ff! " +$strgg % +,gg% & +$strhh " +,hh" # +$strii  +,ii  +$strjj  +,jj  +$strkk  +,kk  +$strll  +,ll  +$strmm  +,mm  +$strnn  +,nn  +$stroo  +,oo  +$strpp  +,pp  +$strqq  +,qq  +$strrr  +,rr  +$strss  +,ss  +$strtt  +,tt  +$struu  +,uu  +$strvv  +,vv  +$strww  +,ww  +$strxx  +,xx  +$stryy  +,yy  +$strzz  +,zz  +$str{{  +,{{  +$str||  +,||  +$str}}  +,}} ! +$str~~  +,~~ ! +$str  +, ! +$str +  +, + ! +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +, +  +$str +  +] +  +; +  +private +  +static +  +readonly +  +Regex + ! +LibraryNamePattern +" 4 += +5 6 +new +7 : +( +: ; +$str +; I +, +I J + RegexOptions +K W +. +W X +Compiled +X ` +| +a b + RegexOptions +c o +. +o p + +IgnoreCase +p z +) +z { +; +{ | +public + + +static +  +RouteGroupBuilder + # +MapORDUAGEndpoints +$ 6 +( +6 7 +this +7 ; +RouteGroupBuilder +< M +group +N S +) +S T +{ +  +group +  +. +  +MapGet +  +( +  +$str + ' +, +' ( +async +) . +( +/ 0 +[ +  + AsParameters +  +] +  + OrduagQuery + * +query ++ 0 +, +0 1 +OdbcConnection +  +conn + # +, +# $ +ILoggerFactory +  + loggerFactory + , +) +, - +=> +. 0 +{ +  +var +  +logger +  += +  + loggerFactory + * +. +* + + CreateLogger ++ 7 +( +7 8 +$str +8 H +) +H I +; +I J +try +  +{ +  +await +  +conn +  +. +  + OpenAsync + ( +( +( ) +) +) * +; +* + +var +  +page +  += +  +query + $ +. +$ % +Page +% ) +. +) * +GetValueOrDefault +* ; +( +; < +$num +< = +) += > +; +> ? +if +  +( +  +page +  +< +  +$num +  +) + ! +{ +  +logger +  +. +  + +LogWarning + ) +( +) * +$str +* V +, +V W +query +X ] +. +] ^ +Page +^ b +) +b c +; +c d +page +  += +  +$num +  +; + ! +} +  +var +  +pageSize +  += +! " +query +# ( +. +( ) +PageSize +) 1 +. +1 2 +GetValueOrDefault +2 C +( +C D +$num +D F +) +F G +; +G H +pageSize +  += +  +Math + # +. +# $ +Clamp +$ ) +( +) * +pageSize +* 2 +, +2 3 +$num +4 5 +, +5 6 +$num +7 : +) +: ; +; +; < +var +  + +offsetLong + " += +# $ +( +% & +long +& * +) +* + +( ++ , +page +, 0 +- +1 2 +$num +3 4 +) +4 5 +* +6 7 +pageSize +8 @ +; +@ A +if +  +( +  + +offsetLong + " +> +# $ +int +% ( +. +( ) +MaxValue +) 1 +) +1 2 +{ +  +return +  +Results + & +. +& ' + +BadRequest +' 1 +( +1 2 +new +2 5 +{ +6 7 +error +8 = += +> ? +$str +@ ^ +} +_ ` +) +` a +; +a b +} +  +var +  +offset +  += +  +( +! " +int +" % +) +% & + +offsetLong +& 0 +; +0 1 +var +  + +sqlBuilder + " += +# $ +new +% ( + StringBuilder +) 6 +( +6 7 +) +7 8 +; +8 9 + +sqlBuilder +  +. +  + +AppendLine + ) +( +) * +$str +* 2 +) +2 3 +; +3 4 + +sqlBuilder +  +. +  + +AppendLine + ) +( +) * +string +* 0 +. +0 1 +Join +1 5 +( +5 6 +$str +6 ; +, +; < + ColumnNames += H +. +H I +Select +I O +( +O P +column +P V +=> +W Y +$" +Z \ +$str +\ ` +{ +` a +column +a g +} +g h +" +h i +) +i j +) +j k +) +k l +; +l m + +sqlBuilder +  +. +  + +AppendLine + ) +( +) * +$str +* ? +) +? @ +; +@ A + +sqlBuilder +  +. +  + +AppendLine + ) +( +) * +$str +* 7 +) +7 8 +; +8 9 +var +  +parameterValues + ' += +( ) +new +* - +List +. 2 +< +2 3 +object +3 9 +? +9 : +> +: ; +( +; < +) +< = +; += > +if +  +( +  +! +  +string +  +. +  +IsNullOrWhiteSpace + 2 +( +2 3 +query +3 8 +. +8 9 + +CodeNumber +9 C +) +C D +) +D E +{ +  + +sqlBuilder + " +. +" # + +AppendLine +# - +( +- . +$str +. H +) +H I +; +I J +parameterValues + ' +. +' ( +Add +( + +( ++ , +query +, 1 +. +1 2 + +CodeNumber +2 < +. +< = +Trim += A +( +A B +) +B C +) +C D +; +D E +} +  +if +  +( +  +! +  +string +  +. +  +IsNullOrWhiteSpace + 2 +( +2 3 +query +3 8 +. +8 9 + AgentAgency +9 D +) +D E +) +E F +{ +  + +sqlBuilder + " +. +" # + +AppendLine +# - +( +- . +$str +. F +) +F G +; +G H +parameterValues + ' +. +' ( +Add +( + +( ++ , +query +, 1 +. +1 2 + AgentAgency +2 = +. += > +Trim +> B +( +B C +) +C D +) +D E +; +E F +} +  +if +  +( +  +! +  +string +  +. +  +IsNullOrWhiteSpace + 2 +( +2 3 +query +3 8 +. +8 9 + AgentStat +9 B +) +B C +) +C D +{ +  + +sqlBuilder + " +. +" # + +AppendLine +# - +( +- . +$str +. D +) +D E +; +E F +parameterValues + ' +. +' ( +Add +( + +( ++ , +query +, 1 +. +1 2 + AgentStat +2 ; +. +; < +Trim +< @ +( +@ A +) +A B +) +B C +; +C D +} +  + +sqlBuilder +  +. +  + +AppendLine + ) +( +) * +$str +* C +) +C D +; +D E + +sqlBuilder +  +. +  + +AppendLine + ) +( +) * +$str +* P +) +P Q +; +Q R +parameterValues + # +. +# $ +Add +$ ' +( +' ( +offset +( . +) +. / +; +/ 0 +parameterValues + # +. +# $ +Add +$ ' +( +' ( +pageSize +( 0 +) +0 1 +; +1 2 +await +  +using +  +var + # +command +$ + += +, - +conn +. 2 +. +2 3 + CreateCommand +3 @ +( +@ A +) +A B +; +B C +command +  +. +  + CommandText + ' += +( ) + +sqlBuilder +* 4 +. +4 5 +ToString +5 = +( += > +) +> ? +; +? @ +foreach +  +( +  +var +  +value +! & +in +' ) +parameterValues +* 9 +) +9 : +{ +  +var +  + parameter + % += +& ' +command +( / +. +/ 0 +CreateParameter +0 ? +( +? @ +) +@ A +; +A B + parameter + ! +. +! " +Value +" ' += +( ) +value +* / +; +/ 0 +command +  +. +  + +Parameters + * +. +* + +Add ++ . +( +. / + parameter +/ 8 +) +8 9 +; +9 : +} +  +var +  +rows +  += +  +new + " +List +# ' +< +' ( + IDictionary +( 3 +< +3 4 +string +4 : +, +: ; +object +< B +> +B C +> +C D +( +D E +) +E F +; +F G +await +  +using +  +( + ! +var +! $ +reader +% + += +, - +await +. 3 +command +4 ; +. +; < +ExecuteReaderAsync +< N +( +N O +) +O P +) +P Q +{ +  +while +  +( +  +await + $ +reader +% + +. ++ , + ReadAsync +, 5 +( +5 6 +) +6 7 +) +7 8 +{ +  +var +  +row + # += +$ % +new +& ) + +Dictionary +* 4 +< +4 5 +string +5 ; +, +; < +object += C +> +C D +( +D E +StringComparer +E S +. +S T +OrdinalIgnoreCase +T e +) +e f +; +f g +for +  +( + ! +var +! $ +i +% & += +' ( +$num +) * +; +* + +i +, - +< +. / +reader +0 6 +. +6 7 + +FieldCount +7 A +; +A B +i +C D +++ +D F +) +F G +{ +  +var + # +value +$ ) += +* + +reader +, 2 +. +2 3 +GetNormalizedValue +3 E +( +E F +i +F G +) +G H +; +H I +row + # +[ +# $ +reader +$ * +. +* + +GetName ++ 2 +( +2 3 +i +3 4 +) +4 5 +] +5 6 += +7 8 +value +9 > +! +> ? +; +? @ +} +  +rows +  +. + ! +Add +! $ +( +$ % +row +% ( +) +( ) +; +) * +} +  +} +  +logger +  +. +  +LogInformation + ) +( +) * +$str   +,   +rows +  +. +  +Count + " +, +" # +query +  +. +  + +CodeNumber + ( +, +( ) +query +  +. +  + AgentAgency + ) +, +) * +query +  +. +  + AgentStat + ' +, +' ( +page +  +, +  +pageSize +  +) + ! +; +! " +var +  + formatted + ! += +" # +rows +$ ( +. +( ) +Select +) / +( +/ 0 +dict +0 4 +=> +5 7 +dict +8 < +. +< =# +ToCamelCaseDictionary += R +( +R S +) +S T +) +T U +. +U V +ToList +V \ +( +\ ] +) +] ^ +; +^ _ +return +  +Results + " +. +" # +Ok +# % +( +% & + formatted +& / +) +/ 0 +; +0 1 +} +  +catch +  +( +  + Exception +  +ex +! # +) +# $ +{ +  +logger +  +. +  +LogError + # +( +# $ +ex +$ & +, +& ' +$str +( Q +) +Q R +; +R S +return +  +Results + " +. +" # +Problem +# * +( +* + +$" ++ - +$str +- D +{ +D E +ex +E G +. +G H +Message +H O +} +O P +" +P Q +) +Q R +; +R S +} +  +} +  +) +  +. +  + WithSummary +  +( +  +$str + M +) +M N +; +N O +return +  +group +  +; +  +} +  +public + + +sealed +  +class +  + OrduagQuery + # +{ +  +public +  +string +  +? +  + +CodeNumber + ! +{ +" # +get +$ ' +; +' ( +init +) - +; +- . +} +/ 0 +public +  +string +  +? +  + AgentAgency + " +{ +# $ +get +% ( +; +( ) +init +* . +; +. / +} +0 1 +public +  +string +  +? +  + AgentStat +  +{ +! " +get +# & +; +& ' +init +( , +; +, - +} +. / +public +  +int +  +? +  +Page +  +{ +  +get +  +; +  +init + $ +; +$ % +} +& ' +public +  +int +  +? +  +PageSize +  +{ +  +get + " +; +" # +init +$ ( +; +( ) +} +* + +} +  +} ( +//workspaces/AS400API/Endpoints/AuthEndpoints.cs + namespace   +AS400API   +  +.    + Endpoints    +;    +public   +static    +class    + AuthEndpoints   ! +{   +public   + +static    +RouteGroupBuilder   # +MapAuthEndpoints  $ 4 +(  4 5 +this  5 9 +RouteGroupBuilder  : K +group  L Q +)  Q R +{  +group  +.  +MapPost  +(  +$str & +,& ' +async( - +Task. 2 +<2 3 +IResult3 : +>: ; +(< = + LoginRequest= I +requestJ Q +,Q R + DemoUserStoreS ` + userStorea j +,j k + TokenServicel x + tokenService y  +, +  + +JwtOptions +  + +jwtOptions +  +) +  +=> +  +{  +if  +(  +request  +is  +null  +|| " +string# ) +.) * +IsNullOrWhiteSpace* < +(< = +request= D +.D E +UsernameE M +)M N +||O Q +stringR X +.X Y +IsNullOrWhiteSpaceY k +(k l +requestl s +.s t +Passwordt | +)| } +)} ~ +{  +return  +Results  +.  + +BadRequest ) +() * +new* - +{. / +error0 5 +=6 7 +$str8 ] +}^ _ +)_ ` +;` a +}  +var  +user  +=  +await  + userStore & +.& ' +FindByNameAsync' 6 +(6 7 +request7 > +.> ? +Username? G +)G H +;H I +if  +(  +user  +is  +null  +||  +! ! + userStore! * +.* + +ValidateCredentials+ > +(> ? +user? C +,C D +requestE L +.L M +PasswordM U +)U V +)V W +{  +return  +Results  +.  + Unauthorized + +(+ , +), - +;- . +}  +var  + accessToken  +=  + tokenService * +.* + + CreateToken+ 6 +(6 7 +user7 ; +); < +;< = +return  +Results  +.  +Ok  +(  +new ! + LoginResponse" / +(/ 0 + accessToken0 ; +,; < + +jwtOptions= G +.G H& +AccessTokenLifetimeMinutesH b +*c d +$nume g +,g h +$stri q +,q r +users w +.w x +Rolesx } +)} ~ +)~  +;   +}  +) + +.  +AllowAnonymous  +(  +)  +.    + WithSummary    +(    +$str   B +)  B C +.!!  +Produces!!  +<!!  + LoginResponse!!  +>!!  +(!! ! + StatusCodes!!! , +.!!, - + Status200OK!!- 8 +)!!8 9 +.""  +Produces""  +(""  + StatusCodes""  +.""  +Status400BadRequest"" 1 +)""1 2 +.##  +Produces##  +(##  + StatusCodes##  +.## ! +Status401Unauthorized## 3 +)##3 4 +;##4 5 +group%%  +.%%  +MapGet%%  +(%%  +$str%% # +,%%# $ +(%%% & +ClaimsPrincipal%%& 5 +user%%6 : +)%%: ; +=>%%< > +{&&  +var''  +username''  +=''  +user''  +.''  +Identity'' ( +?''( ) +.'') * +Name''* . +??''/ 1 +user''2 6 +.''6 7 +FindFirstValue''7 E +(''E F# +JwtRegisteredClaimNames''F ] +.''] ^ +Sub''^ a +)''a b +??''c e +$str''f o +;''o p +var((  +roles((  +=((  +user((  +.((  +FindAll(( $ +((($ % + +ClaimTypes((% / +.((/ 0 +Role((0 4 +)((4 5 +.((5 6 +Select((6 < +(((< = +r((= > +=>((? A +r((B C +.((C D +Value((D I +)((I J +.((J K +ToArray((K R +(((R S +)((S T +;((T U +return))  +Results))  +.))  +Ok))  +())  +new)) ! +{))" # +username))$ , +,)), - +roles)). 3 +}))4 5 +)))5 6 +;))6 7 +}**  +)** + +.++  +RequireAuthorization++  +(++  +)++  +.,,  + WithSummary,,  +(,,  +$str,, O +),,O P +;,,P Q +return..  +group..  +;..  +}//  +}00  +1/workspaces/AS400API/Endpoints/SystemEndpoints.cs + namespace  +AS400API +  +.  + Endpoints  +;  +public   +static    +class    +SystemEndpoints   # +{ + +  +public   + +static   ! +IEndpointRouteBuilder   ' +MapRootEndpoints  ( 8 +(  8 9 +this  9 =! +IEndpointRouteBuilder  > S +app  T W +)  W X +{    +app    +.    +MapGet    +(    +$str    +,    +(    +)    +=>    +Results   % +.  % & +Ok  & ( +(  ( ) +new  ) , +{  - . +name  / 3 +=  4 5 +$str  6 @ +,  @ A +status  B H +=  I J +$str  K O +}  P Q +)  Q R +)  R S +;  S T +return  +app  +;  +}  +public + +static  +RouteGroupBuilder # +MapSystemEndpoints$ 6 +(6 7 +this7 ; +RouteGroupBuilder< M +groupN S +)S T +{  +group  +.  +MapGet  +(  +$str ! +,! " +async# ( +() * +OdbcConnection* 8 +conn9 = +)= > +=>? A +{  +try  +{  +await  +conn  +.  + OpenAsync $ +($ % +)% & +;& ' +using  +var  +cmd  +=  +conn $ +.$ % + CreateCommand% 2 +(2 3 +)3 4 +;4 5 +cmd  +.  + CommandText  += ! +$str" Y +;Y Z +using  +var  +reader  +=! " +await# ( +cmd) , +., - +ExecuteReaderAsync- ? +(? @ +)@ A +;A B +if  +(  +await  +reader  +. ! + ReadAsync! * +(* + +)+ , +), - +{  +var  +d  +=  +reader " +." # + GetDateTime# . +(. / +$num/ 0 +)0 1 +;1 2 +return  +Results " +." # +Ok# % +(% & +new& ) +{* + +AS400, 1 +=2 3 +$str4 < +,< = +currentDateOnAS400> P +=Q R +dS T +.T U +ToStringU ] +(] ^ +$str^ j +)j k +,k l + timestampm v +=w x +DateTime y  +. +  +UtcNow +  +} +  +) +  +; +  +}  +return!!  +Results!!  +.!!  +Ok!! ! +(!!! " +new!!" % +{!!& ' +AS400!!( - +=!!. / +$str!!0 8 +,!!8 9 +note!!: > +=!!? @ +$str!!A J +}!!K L +)!!L M +;!!M N +}""  +catch##  +(##  + Exception##  +ex##  +)##  +{$$  +return%%  +Results%%  +.%%  +Problem%% & +(%%& ' +$"%%' ) +$str%%) B +{%%B C +ex%%C E +.%%E F +Message%%F M +}%%M N +"%%N O +)%%O P +;%%P Q +}&&  +}''  +)'' + +.'' +  +AllowAnonymous''  +(''  +)''  +;''  +return))  +group))  +;))  +}**  +}++ 6 +;/workspaces/AS400API/Infrastructure/DatabaseRowFormatter.cs + namespace  +AS400API +  +.  +Infrastructure ! +;! " +public  +static  +class  +DatabaseRowFormatter ( +{   +public + + + +static + +  + IEnumerable + +  +< + +  + IDictionary + + ) +< + +) * +string + +* 0 +, + +0 1 +object + +2 8 +? + +8 9 +> + +9 : +> + +: ;# +ToCamelCaseDictionaries + +< S +( + +S T +this + +T X + IEnumerable + +Y d +< + +d e +dynamic + +e l +> + +l m +rows + +n r +) + +r s +{    +foreach    +(    +var    +row    +in    +rows    +)   ! +{    +if  +(  +row  +is  + IDictionary " +<" # +string# ) +,) * +object+ 1 +>1 2 +dict3 7 +)7 8 +{  +yield  +return  +dict ! +.! "! +ToCamelCaseDictionary" 7 +(7 8 +)8 9 +;9 : +}  +else  +{  +yield  +return  +new  + +Dictionary! + +<+ , +string, 2 +,2 3 +object4 : +?: ; +>; < +{  +[  +$str  +]  +=  +row # +}  +;  +}  +}  +}  +public + +static  + IDictionary  +<  +string $ +,$ % +object& , +?, - +>- .! +ToCamelCaseDictionary/ D +(D E +thisE I + IDictionaryJ U +<U V +stringV \ +,\ ] +object^ d +>d e +sourcef l +)l m +{  +var  +result  +=  +new  + +Dictionary # +<# $ +string$ * +,* + +object, 2 +?2 3 +>3 4 +(4 5 +StringComparer5 C +.C D +OrdinalIgnoreCaseD U +)U V +;V W +foreach  +(  +var  +kvp  +in  +source " +)" # +{    +var!!  +key!!  +=!!  +string!!  +.!!  +IsNullOrWhiteSpace!! / +(!!/ 0 +kvp!!0 3 +.!!3 4 +Key!!4 7 +)!!7 8 +?!!9 : +kvp!!; > +.!!> ? +Key!!? B +:!!C D + ToCamelCase!!E P +(!!P Q +kvp!!Q T +.!!T U +Key!!U X +)!!X Y +;!!Y Z +result""  +[""  +key""  +]""  +=""  +kvp""  +.""  +Value"" # +;""# $ +}##  +return%%  +result%%  +;%%  +}&&  +private((  +static((  +string((  + ToCamelCase(( % +(((% & +string((& , +input((- 2 +)((2 3 +{))  +if** + +(**  +string**  +.**  + IsNullOrEmpty**  +(** ! +input**! & +)**& ' +)**' ( +{++  +return,,  +input,,  +;,,  +}--  +var//  +segments//  +=//  +input//  +.00  +Split00  +(00  +new00  +[00  +]00  +{00  +$char00  +,00  +$char00 # +,00# $ +$char00% ( +}00) * +,00* + +StringSplitOptions00, > +.00> ? +RemoveEmptyEntries00? Q +)00Q R +.11  +Select11  +(11  +s11  +=>11  +s11  +.11  +ToLowerInvariant11 + +(11+ , +)11, - +)11- . +.22  +ToArray22  +(22  +)22  +;22  +if44 + +(44  +segments44  +.44  +Length44  +==44  +$num44  +)44 ! +{55  +return66  +input66  +;66  +}77  +if99 + +(99  +segments99  +.99  +Length99  +==99  +$num99  +)99 ! +{::  +var;;  +segment;;  +=;;  +segments;; " +[;;" # +$num;;# $ +];;$ % +;;;% & +if<<  +(<<  +segment<<  +.<<  +Length<<  +==<< ! +$num<<" # +)<<# $ +{==  +return>>  +segment>>  +.>>  +ToLowerInvariant>> / +(>>/ 0 +)>>0 1 +;>>1 2 +}??  +returnAA  +charAA  +.AA  +ToLowerInvariantAA ( +(AA( ) +segmentAA) 0 +[AA0 1 +$numAA1 2 +]AA2 3 +)AA3 4 ++AA5 6 +segmentAA7 > +[AA> ? +$numAA? @ +..AA@ B +]AAB C +;AAC D +}BB  +varDD  +sbDD  +=DD  +newDD  + StringBuilderDD " +(DD" # +)DD# $ +;DD$ % +sbEE + +.EE +  +AppendEE  +(EE  +segmentsEE  +[EE  +$numEE  +]EE  +)EE  +;EE  +forFF  +(FF  +varFF  +iFF  +=FF  +$numFF  +;FF  +iFF  +<FF  +segmentsFF $ +.FF$ % +LengthFF% + +;FF+ , +iFF- . +++FF. 0 +)FF0 1 +{GG  +varHH  +segmentHH  +=HH  +segmentsHH " +[HH" # +iHH# $ +]HH$ % +;HH% & +ifII  +(II  +segmentII  +.II  +LengthII  +==II ! +$numII" # +)II# $ +{JJ  +continueKK  +;KK  +}LL  +sbNN  +.NN  +AppendNN  +(NN  +charNN  +.NN  +ToUpperInvariantNN + +(NN+ , +segmentNN, 3 +[NN3 4 +$numNN4 5 +]NN5 6 +)NN6 7 +)NN7 8 +;NN8 9 +ifOO  +(OO  +segmentOO  +.OO  +LengthOO  +>OO  +$numOO! " +)OO" # +{PP  +sbQQ  +.QQ  +AppendQQ  +(QQ  +segmentQQ ! +[QQ! " +$numQQ" # +..QQ# % +]QQ% & +)QQ& ' +;QQ' ( +}RR  +}SS  +returnUU  +sbUU  +.UU  +ToStringUU  +(UU  +)UU  +;UU  +}VV  +}WW  +;/workspaces/AS400API/Infrastructure/DataReaderExtensions.cs + namespace  +AS400API +  +.  +Infrastructure ! +;! " +public  +static  +class  +DataReaderExtensions ( +{  +public + +static  +object  +?  +GetNormalizedValue , +(, - +this- 1 + DbDataReader2 > +reader? E +,E F +intG J +ordinalK R +)R S +{    +if + + + +( + +  +reader + +  +. + +  +IsDBNull + +  +( + +  +ordinal + + # +) + +# $ +) + +$ % +{    +return    +null    +;    +}    +var  +value  +=  +reader  +.  +GetValue # +(# $ +ordinal$ + +)+ , +;, - +if + +(  +value  +is  +string  + stringValue ' +)' ( +{  +var  + dataTypeName  +=  +reader % +.% & +GetDataTypeName& 5 +(5 6 +ordinal6 = +)= > +?> ? +.? @ +Trim@ D +(D E +)E F +;F G +if  +(  +!  +string  +.  + IsNullOrEmpty % +(% & + dataTypeName& 2 +)2 3 +&&  + dataTypeName  +.  + +StartsWith * +(* + +$str+ 1 +,1 2 +StringComparison3 C +.C D +OrdinalIgnoreCaseD U +)U V +&&  +!  + dataTypeName  +. ! +Contains! ) +() * +$str* 3 +,3 4 +StringComparison5 E +.E F +OrdinalIgnoreCaseF W +)W X +)X Y +{  +return  + stringValue " +." # +TrimEnd# * +(* + +)+ , +;, - +}  +}  +return  +value  +;  +}  +} Z +/workspaces/AS400API/Program.cs +var   + builderArgs    +=    +args    +??    +Array    +.    +Empty   % +<  % & +string  & , +>  , - +(  - . +)  . / +;  / 0 +var   +builder    +=    +WebApplication    +.    + CreateBuilder   * +(  * + + builderArgs  + 6 +)  6 7 +;  7 8 +builder  +.  +Host  +.  + +UseSerilog  +(  +(  +context  +, ! +services" * +,* + +loggerConfiguration, ? +)? @ +=>A C +{  +loggerConfiguration  +.  +ReadFrom  +.  + Configuration  +(  +context ' +.' ( + Configuration( 5 +)5 6 +.  +Enrich  +.  +FromLogContext  +(  +)  +; ! +}  +)  +;  +var  +odbc  += + +new  + OdbcOptions  +(  +)  +;  +builder  +.  + Configuration  +.  + +GetSection  +( ! +$str! ' +)' ( +.( ) +Bind) - +(- . +odbc. 2 +)2 3 +;3 4 +odbc  +.  +System  +??=  + Environment  +. " +GetEnvironmentVariable 2 +(2 3 +$str3 A +)A B +;B C +odbc  +.  +DefaultLibraries  +??=  + Environment % +.% &" +GetEnvironmentVariable& < +(< = +$str= V +)V W +;W X +odbc  +.  +User  +??= +  + Environment  +. " +GetEnvironmentVariable 0 +(0 1 +$str1 = +)= > +;> ? +odbc   +.    +Password    +??=    + Environment    +.   " +GetEnvironmentVariable   4 +(  4 5 +$str  5 E +)  E F +;  F G +odbc!!  +.!!  +Naming!!  +??=!!  + Environment!!  +.!! " +GetEnvironmentVariable!! 2 +(!!2 3 +$str!!3 A +)!!A B +??!!C E +$str!!F I +;!!I J +if##  +(##  +string## + +.## +  +IsNullOrWhiteSpace##  +(##  +odbc## " +.##" # +System### ) +)##) * +||##+ - +string$$ + +.$$ +  +IsNullOrWhiteSpace$$  +($$  +odbc$$ " +.$$" # +User$$# ' +)$$' ( +||$$) + +string%% + +.%% +  +IsNullOrWhiteSpace%%  +(%%  +odbc%% " +.%%" # +Password%%# + +)%%+ , +)%%, - +{&&  +Console''  +.''  + WriteLine''  +(''  +$str'' o +)''o p +;''p q +}((  +var++  + +jwtOptions++  +=++  +builder++  +.++  + Configuration++ & +.++& ' + +GetSection++' 1 +(++1 2 + +JwtOptions++2 < +.++< = + SectionName++= H +)++H I +.++I J +Get++J M +<++M N + +JwtOptions++N X +>++X Y +(++Y Z +)++Z [ +??++\ ^ +new++_ b + +JwtOptions++c m +(++m n +)++n o +;++o p + +jwtOptions,, + +.,, +  + EnsureIsValid,,  +(,,  +),,  +;,,  +var--  + +signingKey--  +=--  +new--  +SymmetricSecurityKey-- ) +(--) * +Encoding--* 2 +.--2 3 +UTF8--3 7 +.--7 8 +GetBytes--8 @ +(--@ A + +jwtOptions--A K +.--K L +Key--L O +)--O P +)--P Q +;--Q R +builder//  +.//  +Services//  +.// # +AddEndpointsApiExplorer// ( +(//( ) +)//) * +;//* + +builder00  +.00  +Services00  +.00  + AddSwaggerGen00  +(00  +options00 & +=>00' ) +{11  +options22  +.22  + +SwaggerDoc22  +(22  +$str22  +,22  +new22  + OpenApiInfo22! , +{33  +Title44  +=44  +$str44  +,44  +Version55  +=55  +$str55  +}66  +)66  +;66  +var88  +securityScheme88  +=88  +new88 ! +OpenApiSecurityScheme88 2 +{99  +Name::  +=::  +$str::  +,::  + Description;;  +=;;  +$str;; I +,;;I J +In<< + +=<<  +ParameterLocation<<  +.<<  +Header<< % +,<<% & +Type==  +===  +SecuritySchemeType== ! +.==! " +Http==" & +,==& ' +Scheme>>  +=>>  +$str>>  +,>>  + BearerFormat??  +=??  +$str??  +,??  + Reference@@  +=@@  +new@@  +OpenApiReference@@ ( +{AA  +TypeBB  +=BB  + ReferenceTypeBB  +.BB ! +SecuritySchemeBB! / +,BB/ 0 +IdCC  +=CC  +$strCC  +}DD  +}EE  +;EE  +optionsGG  +.GG ! +AddSecurityDefinitionGG ! +(GG! " +securitySchemeGG" 0 +.GG0 1 + ReferenceGG1 : +.GG: ; +IdGG; = +,GG= > +securitySchemeGG? M +)GGM N +;GGN O +optionsHH  +.HH " +AddSecurityRequirementHH " +(HH" # +newHH# && +OpenApiSecurityRequirementHH' A +{II  +{JJ  +securitySchemeJJ +  +,JJ  +ArrayJJ  +.JJ  +EmptyJJ % +<JJ% & +stringJJ& , +>JJ, - +(JJ- . +)JJ. / +}JJ0 1 +}KK  +)KK  +;KK  +}LL  +)LL  +;LL  +builderMM  +.MM  +ServicesMM  +.MM  + AddSingletonMM  +(MM  +odbcMM " +)MM" # +;MM# $ +builderNN  +.NN  +ServicesNN  +.NN  + AddScopedNN  +<NN  +OdbcConnectionNN ) +>NN) * +(NN* + +_NN+ , +=>NN- / +newNN0 3 +OdbcConnectionNN4 B +(NNB C +odbcNNC G +.NNG H +ToConnectionStringNNH Z +(NNZ [ +)NN[ \ +)NN\ ] +)NN] ^ +;NN^ _ +builderOO  +.OO  +ServicesOO  +.OO  + AddSingletonOO  +(OO  + +jwtOptionsOO ( +)OO( ) +;OO) * +builderPP  +.PP  +ServicesPP  +.PP  + AddSingletonPP  +<PP  + TokenServicePP * +>PP* + +(PP+ , +)PP, - +;PP- . +builderQQ  +.QQ  +ServicesQQ  +.QQ  + AddSingletonQQ  +<QQ  + DemoUserStoreQQ + +>QQ+ , +(QQ, - +)QQ- . +;QQ. / +builderSS  +.SS  +ServicesSS  +.TT  +AddAuthenticationTT  +(TT  +JwtBearerDefaultsTT ( +.TT( ) +AuthenticationSchemeTT) = +)TT= > +.UU  + AddJwtBearerUU  +(UU  +optionsUU  +=>UU  +{VV  +optionsWW  +.WW % +TokenValidationParametersWW ) +=WW* + +newWW, /% +TokenValidationParametersWW0 I +{XX  +ValidateIssuerYY  +=YY  +trueYY ! +,YY! " + ValidIssuerZZ  +=ZZ  + +jwtOptionsZZ $ +.ZZ$ % +IssuerZZ% + +,ZZ+ , +ValidateAudience[[  +=[[  +true[[ # +,[[# $ + ValidAudience\\  +=\\  + +jwtOptions\\ & +.\\& ' +Audience\\' / +,\\/ 0$ +ValidateIssuerSigningKey]] $ +=]]% & +true]]' + +,]]+ , +IssuerSigningKey^^  +=^^  + +signingKey^^ ) +,^^) * +ValidateLifetime__  +=__  +true__ # +,__# $ + ClockSkew``  +=``  +TimeSpan``  +.`` ! + FromMinutes``! , +(``, - +$num``- . +)``. / +}aa  +;aa + +}bb  +)bb  +;bb  +builderdd  +.dd  +Servicesdd  +.dd  +AddAuthorizationdd ! +(dd! " +optionsdd" ) +=>dd* , +{ee  +optionsff  +.ff  + AddPolicyff  +(ff  + AuthPoliciesff " +.ff" # +RequireOperatorff# 2 +,ff2 3 +policyff4 : +=>ff; = +policygg  +.gg  + RequireRolegg  +(gg  +Rolesgg  +.gg ! +Admingg! & +,gg& ' +Rolesgg( - +.gg- . +Operatorgg. 6 +)gg6 7 +)gg7 8 +;gg8 9 +optionshh  +.hh  + AddPolicyhh  +(hh  + AuthPolicieshh " +.hh" # + RequireAdminhh# / +,hh/ 0 +policyhh1 7 +=>hh8 : +policyii  +.ii  + RequireRoleii  +(ii  +Rolesii  +.ii ! +Adminii! & +)ii& ' +)ii' ( +;ii( ) +}jj  +)jj  +;jj  +varll  +appll  +=ll  +builderll +  +.ll  +Buildll  +(ll  +)ll  +;ll  +ifnn  +(nn  +appnn  +.nn  + Environmentnn  +.nn  + IsDevelopmentnn ! +(nn! " +)nn" # +)nn# $ +{oo  +apppp  +.pp  + +UseSwaggerpp  +(pp  +)pp  +;pp  +appqq  +.qq  + UseSwaggerUIqq  +(qq  +)qq  +;qq  +}rr  +apptt  +.tt $ +UseSerilogRequestLoggingtt  +(tt  +)tt  +;tt  +appvv  +.vv  +UseAuthenticationvv  +(vv  +)vv  +;vv  +appww  +.ww  +UseAuthorizationww  +(ww  +)ww  +;ww  +appyy  +.yy  +MapRootEndpointsyy  +(yy  +)yy  +;yy  +var{{  +api{{  +={{  +app{{ +  +.{{  +MapGroup{{  +({{  +$str{{  +){{  +;{{  +api||  +.||  +MapSystemEndpoints||  +(||  +)||  +;||  +api}}  +.}}  +MapAuthEndpoints}}  +(}}  +)}}  +;}}  +api~~  +.~~  +MapAs400Endpoints~~  +(~~  +)~~  +;~~  +api  +.  +MapORDUAGEndpoints  +(  +)  +;  +app  +. +  +Run +  +( +  +) +  +; + diff --git a/.sonarqube/out/0/output-cs/token-type.pb b/.sonarqube/out/0/output-cs/token-type.pb new file mode 100644 index 0000000..f7db817 --- /dev/null +++ b/.sonarqube/out/0/output-cs/token-type.pb @@ -0,0 +1,437 @@ + +)/workspaces/AS400API/Auth/AuthPolicies.cs +  +         +     * ;  +     ' 5 +%/workspaces/AS400API/Auth/DemoUser.cs +  +  +         +     % + : @ O b c i  +      +   ! $  +   ! $  +    % / 2 +*/workspaces/AS400API/Auth/DemoUserStore.cs +  +  +  +  +       !                &   ( 0    +                &   ( 0   2 @   # * , 6 8 ; @ E M R   & 0 2 < > A F K  +     0 6 % ( ) ,      +   $ , 3 9         ' - 8 > I \ ] c    )             +)/workspaces/AS400API/Auth/LoginRequest.cs +  +       ! " ( 3 9 +*/workspaces/AS400API/Auth/LoginResponse.cs +  +  +       " # ) 7 : F L X k l r ++/workspaces/AS400API/Auth/PasswordHasher.cs +  +  +  +  +       "               ! #  + +   + +   + +   + + "               # *    +            &   : @    (    % = M     3 :  +      $ / 5 B H        ) F V    !    & +"/workspaces/AS400API/Auth/Roles.cs +  +         +      '  +     # - +)/workspaces/AS400API/Auth/TokenService.cs +  +  +  +  +  +  +  +   +                                , = @  +    "  +    &   ! $ % 7    $ % -          #    '    ' - 1     ""  ""  ""  $$  $$  $$! + ''  ''  '' ( ++  ..  +0/workspaces/AS400API/Configuration/JwtOptions.cs +  +  +  +         +     & +  + + +  + +   + +   + + #  + +) 3    +           " %   + =    +               & I    +       , /   1 4   : <  +    +   . 6 P R      / 0   + * +      / 0 k +1/workspaces/AS400API/Configuration/OdbcOptions.cs +  +  +  +         +     ! $    +       & )   + .  + + +  + +   + +   + + "    +        !   # &        Z    +     ! $  +    " $ ' - 0  +   " % ' * 0 7  +      " ( ,  +    @    $ < O T n        #     # % % &        +   D F F W i j  +   8 : : > D E  +   < > > B L M    +       : <   < C   K L !! + !!  !!= ? !!? I !!T U "" + ""  ""@ B ""B P ""^ _ ## + ## - $$  $$  $$  +0/workspaces/AS400API/Endpoints/As400Endpoints.cs +  +  +  +  +  +  +  +  +   + + +  +   +       "  +    # 6 : ; L  & ( - / 5 ; I P ^   4 H         & _    " . 1 < ` & W h i    !     !!& M ""  ""  $$  $$  &&$ 9 ''  ''  ''' ) '') 7 ''C D ** * ++ Q ,,  --  // * //, 1 //3 A //H V 11  114 L 22  44  55  55  55( x 66  66 $ 77  88& F 99  99  ;;  ;;  ==$ C >>  >>  >>' ) >>) A >>M N AA * BB K DD ' DD) . DD0 6 DDD R DDY g FF  FF4 I GG  II  JJ  JJ  LL& _ MM  MM " MM. 1 MM< h PP  PP  PU(  WW  WW # WW> A XX& [ YY  ZZ  ZZ  \\  \\  ^^$ X __  __  __' ) __) @ __L M bb * cc I ee 0 ee2 7 ee9 ? eeM S ee_ m  eet  gg  gg4 Q hh  jj  kk  kk  kk> D mm& x nn  nn " nn. 1 nn< z qq  qq  qw(  yy  yy  yy  ||  +  +  + ! +" & +' 2 +3 9 +; A +  +  +  +* / +  +  +  +" % +& 0 +1 7 +9 ? +A O +  +  +% & + ` + 3 +& U +  +  +  +  +  +$ R +  +  +' ) +) A +M N + > + X + " +6/workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs +  +  +  +  +  +  +  +  +   + + +  +   +   +       "        "                               !!  ""  ##  $$  %%  &&  ''  ((  ))  **  ++  ,,  --  ..  //  00  11  22 # 33  44  55  66  77  88  99  ::  ;;  << # ==  >>  ??  @@  AA  BB  CC  DD  EE  FF  GG  HH  II  JJ  KK " LL " MM " NN " OO " PP " QQ " RR " SS " TT # UU # VV # WW " XX  YY $ ZZ ! [[ $ \\ ! ]] " ^^  __ ! ``  aa ! bb  cc  dd  ee $ ff ! gg % hh " ii  jj  kk  ll  mm  nn  oo  pp  qq  rr  ss  tt  uu  vv  ww  xx  yy  zz  {{  ||  }}  ~~    +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + ! +7 : +; I +K W +c o + + +  + # +7 ; +< M + ' +) . +  + * +  +  +  +8 H +  +  +  +< = +  +  +* V +  +  +D F + # +4 5 +7 : +  +& * +3 4 +  +% ( +  + & +2 5 +@ ^ +  +" % +  +% ( +) 6 +* 2 +* 0 +6 ; +Z \ +h i +* ? +* 7 +  +* - +. 2 +3 9 +  +  +. H +  +  +. F +  +  +. D +* C +* P +  +  + # +  +  +' ) +  +  + " +# ' +( 3 +4 : +< B +  +  +! $ +. 3 +  + $ +  +& ) +* 4 +5 ; += C +E S +  +! $ +) * + #   +  +  + " +  +  +( Q +  + " ++ - +- D +P Q + B + M +  + + +  +  + # +  +  +$ ' +) - +  +  +% ( +* . +  +  +# & +( , +  +  +  + $ +  +  + " +$ ( +//workspaces/AS400API/Endpoints/AuthEndpoints.cs +  +  +  +  +  +  +  +   +              !    +        #   5 9   : K  & ( - . 2 3 : = I S ` l x +        # ) R X     * - 8 ]                      ! " / e g i q    B !!  !!! , ""  ##  %% # %%& 5 ''  ''F ] ''f o ((  ((% / ))  ))  )) ! ,, O ..  +1/workspaces/AS400API/Endpoints/SystemEndpoints.cs +  +  +  +  +  +  +              #    +        '   9 =   > S        %   ) ,   6 @   K O    +    # 7 ; < M  ! # ( * 8         " Y     # (       / 0    " & ) 4 < ^ j  y  !!  !!  !!" % !!0 8 !!A J ##  ##  %%  %%  %%' ) %%) B %%N O ))  +;/workspaces/AS400API/Infrastructure/DatabaseRowFormatter.cs +  +  +  +  +  +       (  + + +  + +   + +   + + )  + +* 0  + +2 8  + +T X  + +Y d  + +e l                  " # ) + 1             ! + , 2 4 :    +      $ & , E I J U V \ ^ d      # $ * , 2 5 C       !!  !!  %%  ((  ((  ((  ((& , ** + **  ,,  //  00  00  00 # 00% ( 00, > 44 + 44  66  99 + 99  ;;  ;;# $ <<  <<" # >>  AA  AA  AA1 2 AA? @ DD  DD  DD " EE  FF  FF  FF  HH  II  II" # KK  NN  NN4 5 OO  OO! " QQ" # UU  +;/workspaces/AS400API/Infrastructure/DataReaderExtensions.cs +  +  +  +       (  +     - 1 2 > G J  + + +            +           + 1 3 C * 3 5 E     + +/workspaces/AS400API/Program.cs +  +  +  +  +  +  +  +  +   +  4 +         & , +       +  +      ! ' + ,   3 A  % = V   1 =       5 E !!  !!3 A !!F I +##  ## + $$ + %% + ''  '' o +**  +++  ++2 < ++N X ++_ b ++c m +--  --  -- ) --* 2 22  22  22! , 44  55  88  88  88 2 ::  ;; I <<  == ! >>  ??  @@  @@ ( BB  CC  HH# & HH' A JJ  JJ& , NN ) NN0 3 NN4 B PP * QQ + TT ( WW, / WW0 I YY ! [[ # ]]' + __ # ``  ``- . ff " gg  gg( - hh " ii  +ll  +nn  +{{  {{   1  F \ No newline at end of file diff --git a/.sonarqube/out/ProjectInfo.log b/.sonarqube/out/ProjectInfo.log new file mode 100644 index 0000000..c6b7312 --- /dev/null +++ b/.sonarqube/out/ProjectInfo.log @@ -0,0 +1,24 @@ +Product projects +--------------------------------------- +/workspaces/AS400API/AS400API.csproj + + +Test projects +--------------------------------------- + + +Invalid projects +--------------------------------------- +{none} + + +Skipped projects +--------------------------------------- +{none} + + +Excluded projects +--------------------------------------- +{none} + + diff --git a/.sonarqube/out/ScannerEngineInput.json b/.sonarqube/out/ScannerEngineInput.json new file mode 100644 index 0000000..0a10269 --- /dev/null +++ b/.sonarqube/out/ScannerEngineInput.json @@ -0,0 +1,104 @@ +{ + "scannerProperties": [ + { + "key": "sonar.scanner.app", + "value": "ScannerMSBuild" + }, + { + "key": "sonar.scanner.appVersion", + "value": "10.4.1" + }, + { + "key": "sonar.projectKey", + "value": "as400api" + }, + { + "key": "sonar.projectName", + "value": "AS400API" + }, + { + "key": "sonar.working.directory", + "value": "/workspaces/AS400API/.sonarqube/out/.sonar" + }, + { + "key": "sonar.projectBaseDir", + "value": "/workspaces/AS400API" + }, + { + "key": "sonar.pullrequest.cache.basepath", + "value": "/workspaces/AS400API" + }, + { + "key": "sonar.sources", + "value": "" + }, + { + "key": "sonar.tests", + "value": "" + }, + { + "key": "sonar.modules", + "value": "40CB5B53-77B8-B1FD-458C-805350CB09E8" + }, + { + "key": "40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.projectKey", + "value": "as400api:40CB5B53-77B8-B1FD-458C-805350CB09E8" + }, + { + "key": "40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.projectName", + "value": "AS400API" + }, + { + "key": "40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.projectBaseDir", + "value": "/workspaces/AS400API" + }, + { + "key": "40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.working.directory", + "value": "/workspaces/AS400API/.sonarqube/out/.sonar/mod0" + }, + { + "key": "40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.sourceEncoding", + "value": "utf-8" + }, + { + "key": "40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.tests", + "value": "" + }, + { + "key": "40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.sources", + "value": "/workspaces/AS400API/Auth/AuthPolicies.cs,/workspaces/AS400API/Auth/DemoUser.cs,/workspaces/AS400API/Auth/DemoUserStore.cs,/workspaces/AS400API/Auth/LoginRequest.cs,/workspaces/AS400API/Auth/LoginResponse.cs,/workspaces/AS400API/Auth/PasswordHasher.cs,/workspaces/AS400API/Auth/Roles.cs,/workspaces/AS400API/Auth/TokenService.cs,/workspaces/AS400API/Configuration/JwtOptions.cs,/workspaces/AS400API/Configuration/OdbcOptions.cs,/workspaces/AS400API/Endpoints/As400Endpoints.cs,/workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs,/workspaces/AS400API/Endpoints/AuthEndpoints.cs,/workspaces/AS400API/Endpoints/SystemEndpoints.cs,/workspaces/AS400API/Infrastructure/DatabaseRowFormatter.cs,/workspaces/AS400API/Infrastructure/DataReaderExtensions.cs,/workspaces/AS400API/Program.cs,/workspaces/AS400API/obj/Debug/net9.0/AS400API.GlobalUsings.g.cs,\"/workspaces/AS400API/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs\",/workspaces/AS400API/obj/Debug/net9.0/AS400API.AssemblyInfo.cs,/workspaces/AS400API/obj/Debug/net9.0/AS400API.MvcApplicationPartsAssemblyInfo.cs,/workspaces/AS400API/appsettings.json,/workspaces/AS400API/.dockerignore,/workspaces/AS400API/docker-compose.yml,/workspaces/AS400API/docker/odbc/odbc.ini,/workspaces/AS400API/docker/odbc/odbcinst.ini,/workspaces/AS400API/Dockerfile,/workspaces/AS400API/drivers/ibm-iaccess-1.1.0.28-1.0.x86_64.rpm,/workspaces/AS400API/Logs/as400-api-20251001.log,/workspaces/AS400API/Logs/as400-api-20251002.log,/workspaces/AS400API/Logs/as400-api-20251003.log,/workspaces/AS400API/Logs/as400-api-20251004.log,/workspaces/AS400API/README.md,/workspaces/AS400API/scripts/run-sonar.sh,/workspaces/AS400API/scripts/test-databases.sh,/workspaces/AS400API/obj/AS400API.csproj.nuget.dgspec.json,/workspaces/AS400API/obj/project.assets.json,/workspaces/AS400API/.devcontainer/Dockerfile,/workspaces/AS400API/.devcontainer/devcontainer.json,/workspaces/AS400API/.vscode/launch.json,/workspaces/AS400API/.vscode/tasks.json,/workspaces/AS400API/obj/Debug/net9.0/staticwebassets.build.json,/workspaces/AS400API/obj/Debug/net9.0/staticwebassets.build.endpoints.json,/workspaces/AS400API/obj/Debug/net9.0/rjsmrazor.dswa.cache.json,/workspaces/AS400API/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json,/workspaces/AS400API/obj/Debug/net9.0/rpswa.dswa.cache.json,/workspaces/AS400API/bin/Debug/net9.0/AS400API.deps.json,/workspaces/AS400API/bin/Debug/net9.0/appsettings.json,/workspaces/AS400API/bin/Debug/net9.0/AS400API.runtimeconfig.json,/workspaces/AS400API/bin/Debug/net9.0/AS400API.staticwebassets.endpoints.json" + }, + { + "key": "40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.cs.analyzer.projectOutPaths", + "value": "/workspaces/AS400API/.sonarqube/out/0" + }, + { + "key": "40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.cs.roslyn.reportFilePaths", + "value": "/workspaces/AS400API/.sonarqube/out/0/Issues.json" + }, + { + "key": "40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.cs.scanner.telemetry", + "value": "/workspaces/AS400API/.sonarqube/out/0/Telemetry.json" + }, + { + "key": "sonar.login", + "value": "***" + }, + { + "key": "sonar.host.url", + "value": "http://host.docker.internal:9000" + }, + { + "key": "sonar.cs.opencover.reportsPaths", + "value": "**/coverage.opencover.xml" + }, + { + "key": "sonar.exclusions", + "value": "**/bin/**,**/obj/**,**/coverage.opencover.xml" + }, + { + "key": "sonar.visualstudio.enable", + "value": "false" + } + ] +} \ No newline at end of file diff --git a/.sonarqube/out/Telemetry.S4NET.json b/.sonarqube/out/Telemetry.S4NET.json new file mode 100644 index 0000000..a86cab7 --- /dev/null +++ b/.sonarqube/out/Telemetry.S4NET.json @@ -0,0 +1,7 @@ +{"dotnetenterprise.s4net.params.sonar_cs_opencover_reportspaths.source":"CLI"} +{"dotnetenterprise.s4net.params.sonar_exclusions.source":"CLI"} +{"dotnetenterprise.s4net.serverInfo.product":"SQ_Server"} +{"dotnetenterprise.s4net.serverInfo.serverUrl":"custom_url"} +{"dotnetenterprise.s4net.serverInfo.version":"9.9.8.100196"} +{"dotnetenterprise.s4net.jre.bootstrapping":"UnsupportedByServer"} +{"dotnetenterprise.s4net.scannerEngine.bootstrapping":"Unsupported"} diff --git a/.sonarqube/out/Telemetry.Targets.S4NET.json b/.sonarqube/out/Telemetry.Targets.S4NET.json new file mode 100644 index 0000000..a9a50df --- /dev/null +++ b/.sonarqube/out/Telemetry.Targets.S4NET.json @@ -0,0 +1,2 @@ +{"dotnetenterprise.s4net.build.visual_studio_version":"17.0"} +{"dotnetenterprise.s4net.build.msbuild_version":"17.14.21"} diff --git a/.sonarqube/out/sonar-project.properties b/.sonarqube/out/sonar-project.properties new file mode 100644 index 0000000..313a873 --- /dev/null +++ b/.sonarqube/out/sonar-project.properties @@ -0,0 +1,78 @@ +sonar.projectKey=as400api +sonar.projectName=AS400API +sonar.working.directory=/workspaces/AS400API/.sonarqube/out/.sonar +sonar.projectBaseDir=/workspaces/AS400API +sonar.pullrequest.cache.basepath=/workspaces/AS400API + +40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.projectKey=as400api:40CB5B53-77B8-B1FD-458C-805350CB09E8 +40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.projectName=AS400API +40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.projectBaseDir=/workspaces/AS400API +40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.sourceEncoding=utf-8 +40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.tests= +40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.sources=\ +"/workspaces/AS400API/Auth/AuthPolicies.cs",\ +"/workspaces/AS400API/Auth/DemoUser.cs",\ +"/workspaces/AS400API/Auth/DemoUserStore.cs",\ +"/workspaces/AS400API/Auth/LoginRequest.cs",\ +"/workspaces/AS400API/Auth/LoginResponse.cs",\ +"/workspaces/AS400API/Auth/PasswordHasher.cs",\ +"/workspaces/AS400API/Auth/Roles.cs",\ +"/workspaces/AS400API/Auth/TokenService.cs",\ +"/workspaces/AS400API/Configuration/JwtOptions.cs",\ +"/workspaces/AS400API/Configuration/OdbcOptions.cs",\ +"/workspaces/AS400API/Endpoints/As400Endpoints.cs",\ +"/workspaces/AS400API/Endpoints/AS400_CP3FPRD/ORDUAG.cs",\ +"/workspaces/AS400API/Endpoints/AuthEndpoints.cs",\ +"/workspaces/AS400API/Endpoints/SystemEndpoints.cs",\ +"/workspaces/AS400API/Infrastructure/DatabaseRowFormatter.cs",\ +"/workspaces/AS400API/Infrastructure/DataReaderExtensions.cs",\ +"/workspaces/AS400API/Program.cs",\ +"/workspaces/AS400API/obj/Debug/net9.0/AS400API.GlobalUsings.g.cs",\ +"/workspaces/AS400API/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs",\ +"/workspaces/AS400API/obj/Debug/net9.0/AS400API.AssemblyInfo.cs",\ +"/workspaces/AS400API/obj/Debug/net9.0/AS400API.MvcApplicationPartsAssemblyInfo.cs",\ +"/workspaces/AS400API/appsettings.json",\ +"/workspaces/AS400API/.dockerignore",\ +"/workspaces/AS400API/docker-compose.yml",\ +"/workspaces/AS400API/docker/odbc/odbc.ini",\ +"/workspaces/AS400API/docker/odbc/odbcinst.ini",\ +"/workspaces/AS400API/Dockerfile",\ +"/workspaces/AS400API/drivers/ibm-iaccess-1.1.0.28-1.0.x86_64.rpm",\ +"/workspaces/AS400API/Logs/as400-api-20251001.log",\ +"/workspaces/AS400API/Logs/as400-api-20251002.log",\ +"/workspaces/AS400API/Logs/as400-api-20251003.log",\ +"/workspaces/AS400API/Logs/as400-api-20251004.log",\ +"/workspaces/AS400API/README.md",\ +"/workspaces/AS400API/scripts/run-sonar.sh",\ +"/workspaces/AS400API/scripts/test-databases.sh",\ +"/workspaces/AS400API/obj/AS400API.csproj.nuget.dgspec.json",\ +"/workspaces/AS400API/obj/project.assets.json",\ +"/workspaces/AS400API/.devcontainer/Dockerfile",\ +"/workspaces/AS400API/.devcontainer/devcontainer.json",\ +"/workspaces/AS400API/.vscode/launch.json",\ +"/workspaces/AS400API/.vscode/tasks.json",\ +"/workspaces/AS400API/obj/Debug/net9.0/staticwebassets.build.json",\ +"/workspaces/AS400API/obj/Debug/net9.0/staticwebassets.build.endpoints.json",\ +"/workspaces/AS400API/obj/Debug/net9.0/rjsmrazor.dswa.cache.json",\ +"/workspaces/AS400API/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json",\ +"/workspaces/AS400API/obj/Debug/net9.0/rpswa.dswa.cache.json",\ +"/workspaces/AS400API/bin/Debug/net9.0/AS400API.deps.json",\ +"/workspaces/AS400API/bin/Debug/net9.0/appsettings.json",\ +"/workspaces/AS400API/bin/Debug/net9.0/AS400API.runtimeconfig.json",\ +"/workspaces/AS400API/bin/Debug/net9.0/AS400API.staticwebassets.endpoints.json" + +40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.cs.analyzer.projectOutPaths=\ +"/workspaces/AS400API/.sonarqube/out/0" +40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.cs.roslyn.reportFilePaths=\ +"/workspaces/AS400API/.sonarqube/out/0/Issues.json" +40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.cs.scanner.telemetry=\ +"/workspaces/AS400API/.sonarqube/out/0/Telemetry.json" + +40CB5B53-77B8-B1FD-458C-805350CB09E8.sonar.working.directory=/workspaces/AS400API/.sonarqube/out/.sonar/mod0 +sonar.host.url=http://host.docker.internal:9000 +sonar.cs.opencover.reportsPaths=**/coverage.opencover.xml +sonar.exclusions=**/bin/**,**/obj/**,**/coverage.opencover.xml +sonar.visualstudio.enable=false + +sonar.modules=40CB5B53-77B8-B1FD-458C-805350CB09E8 + diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f3b3ed8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + "version": "0.2.0", + "configurations": [ + + { + + "name": "AS400API: Launch", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "dotnet", + "args": [ "run", "--project", "${workspaceFolder}/AS400API.csproj" ], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://[^\\s]+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://0.0.0.0:5080" + } + }, + { + "name": "AS400API: Attach", + "type": "coreclr", + "request": "attach" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..7e220b6 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,32 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/AS400API.csproj" + ], + "problemMatcher": "\u0024msCompile", + "group": "build", + "presentation": { + "reveal": "silent" + } + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/AS400API.csproj" + ], + "isBackground": true, + "problemMatcher": "\u0024msCompile" + } + ] +} diff --git a/AS400API.csproj b/AS400API.csproj new file mode 100644 index 0000000..96f19c1 --- /dev/null +++ b/AS400API.csproj @@ -0,0 +1,17 @@ + + + net9.0 + enable + enable + true + + + + + + + + + + + diff --git a/AS400API.sln b/AS400API.sln new file mode 100644 index 0000000..53f84e0 --- /dev/null +++ b/AS400API.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AS400API", "AS400API.csproj", "{40CB5B53-77B8-B1FD-458C-805350CB09E8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {40CB5B53-77B8-B1FD-458C-805350CB09E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40CB5B53-77B8-B1FD-458C-805350CB09E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40CB5B53-77B8-B1FD-458C-805350CB09E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40CB5B53-77B8-B1FD-458C-805350CB09E8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {766AFEF9-A58F-4ED4-87F7-701B37E1F580} + EndGlobalSection +EndGlobal diff --git a/Auth/AuthPolicies.cs b/Auth/AuthPolicies.cs new file mode 100644 index 0000000..f915af5 --- /dev/null +++ b/Auth/AuthPolicies.cs @@ -0,0 +1,7 @@ +namespace AS400API.Auth; + +public static class AuthPolicies +{ + public const string RequireOperator = "RequireOperator"; + public const string RequireAdmin = "RequireAdmin"; +} diff --git a/Auth/DemoUser.cs b/Auth/DemoUser.cs new file mode 100644 index 0000000..013d9b5 --- /dev/null +++ b/Auth/DemoUser.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace AS400API.Auth; + +public sealed class DemoUser +{ + public DemoUser(string username, string passwordHash, string passwordSalt, IReadOnlyCollection roles) + { + Username = username; + PasswordHash = passwordHash; + PasswordSalt = passwordSalt; + Roles = roles; + } + + public string Username { get; } + public string PasswordHash { get; } + public string PasswordSalt { get; } + public IReadOnlyCollection Roles { get; } +} diff --git a/Auth/DemoUserStore.cs b/Auth/DemoUserStore.cs new file mode 100644 index 0000000..118be2b --- /dev/null +++ b/Auth/DemoUserStore.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace AS400API.Auth; + +public sealed class DemoUserStore +{ + private readonly Dictionary _users; + + public DemoUserStore() + { + _users = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["admin"] = CreateUser("admin", "Pass@123", new[] { Roles.Admin, Roles.Operator }), + ["operator"] = CreateUser("operator", "Pass@123", new[] { Roles.Operator }) + }; + } + + public ValueTask FindByNameAsync(string username) + { + _users.TryGetValue(username, out var user); + return ValueTask.FromResult(user); + } + + public bool ValidateCredentials(DemoUser user, string password) => + PasswordHasher.Verify(password, user.PasswordHash, user.PasswordSalt); + + private static DemoUser CreateUser(string username, string password, IReadOnlyCollection roles) + { + var (hash, salt) = PasswordHasher.HashPassword(password); + return new DemoUser(username, hash, salt, roles); + } +} diff --git a/Auth/LoginRequest.cs b/Auth/LoginRequest.cs new file mode 100644 index 0000000..07cb4cb --- /dev/null +++ b/Auth/LoginRequest.cs @@ -0,0 +1,3 @@ +namespace AS400API.Auth; + +public sealed record LoginRequest(string Username, string Password); diff --git a/Auth/LoginResponse.cs b/Auth/LoginResponse.cs new file mode 100644 index 0000000..30fe262 --- /dev/null +++ b/Auth/LoginResponse.cs @@ -0,0 +1,5 @@ +using System.Collections.Generic; + +namespace AS400API.Auth; + +public sealed record LoginResponse(string AccessToken, int ExpiresIn, string TokenType, IReadOnlyCollection Roles); diff --git a/Auth/PasswordHasher.cs b/Auth/PasswordHasher.cs new file mode 100644 index 0000000..708d372 --- /dev/null +++ b/Auth/PasswordHasher.cs @@ -0,0 +1,27 @@ +using System; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; + +namespace AS400API.Auth; + +public static class PasswordHasher +{ + private const int SaltSize = 16; + private const int KeySize = 32; + private const int Iterations = 100_000; + + public static (string Hash, string Salt) HashPassword(string password) + { + var salt = RandomNumberGenerator.GetBytes(SaltSize); + var hashBytes = KeyDerivation.Pbkdf2(password, salt, KeyDerivationPrf.HMACSHA256, Iterations, KeySize); + return (Convert.ToBase64String(hashBytes), Convert.ToBase64String(salt)); + } + + public static bool Verify(string password, string storedHash, string storedSalt) + { + var saltBytes = Convert.FromBase64String(storedSalt); + var computedBytes = KeyDerivation.Pbkdf2(password, saltBytes, KeyDerivationPrf.HMACSHA256, Iterations, KeySize); + var storedBytes = Convert.FromBase64String(storedHash); + return CryptographicOperations.FixedTimeEquals(storedBytes, computedBytes); + } +} diff --git a/Auth/Roles.cs b/Auth/Roles.cs new file mode 100644 index 0000000..f70e9d2 --- /dev/null +++ b/Auth/Roles.cs @@ -0,0 +1,7 @@ +namespace AS400API.Auth; + +public static class Roles +{ + public const string Admin = "Admin"; + public const string Operator = "Operator"; +} diff --git a/Auth/TokenService.cs b/Auth/TokenService.cs new file mode 100644 index 0000000..ad4074a --- /dev/null +++ b/Auth/TokenService.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using AS400API.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace AS400API.Auth; + +public sealed class TokenService +{ + private readonly JwtOptions _options; + private readonly JwtSecurityTokenHandler _tokenHandler = new(); + + public TokenService(JwtOptions options) + { + _options = options; + } + + public string CreateToken(DemoUser user) + { + var signingCredentials = new SigningCredentials( + new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Key)), + SecurityAlgorithms.HmacSha256); + + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, user.Username), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new(ClaimTypes.Name, user.Username) + }; + + foreach (var role in user.Roles) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + } + + var token = new JwtSecurityToken( + issuer: _options.Issuer, + audience: _options.Audience, + claims: claims, + expires: DateTime.UtcNow.AddMinutes(_options.AccessTokenLifetimeMinutes), + signingCredentials: signingCredentials); + + return _tokenHandler.WriteToken(token); + } +} diff --git a/Configuration/JwtOptions.cs b/Configuration/JwtOptions.cs new file mode 100644 index 0000000..7cf9e9b --- /dev/null +++ b/Configuration/JwtOptions.cs @@ -0,0 +1,27 @@ +using System; +using System.Text; + +namespace AS400API.Configuration; + +public sealed class JwtOptions +{ + public const string SectionName = "Jwt"; + + public string Issuer { get; set; } = "AS400API"; + public string Audience { get; set; } = "AS400API.Clients"; + public string Key { get; set; } = "change-me-to-a-long-random-secret"; + public int AccessTokenLifetimeMinutes { get; set; } = 60; + + public void EnsureIsValid() + { + if (string.IsNullOrWhiteSpace(Key) || Encoding.UTF8.GetByteCount(Key) < 32) + { + throw new InvalidOperationException("Jwt:Key must be at least 32 UTF-8 bytes. Set a strong secret in configuration"); + } + + if (AccessTokenLifetimeMinutes <= 0) + { + throw new InvalidOperationException("Jwt:AccessTokenLifetimeMinutes must be a positive integer"); + } + } +} diff --git a/Configuration/OdbcOptions.cs b/Configuration/OdbcOptions.cs new file mode 100644 index 0000000..4c7b9c7 --- /dev/null +++ b/Configuration/OdbcOptions.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace AS400API.Configuration; + +public sealed class OdbcOptions +{ + public string? System { get; set; } + public string? DefaultLibraries { get; set; } + public string? User { get; set; } + public string? Password { get; set; } + /// + /// 0 = SQL naming (*SYS = library/file); 1 = System naming (*SYS). Keep 1 by default. + /// + public string? Naming { get; set; } + public string? Translate { get; set; } = "1"; + public string? ClientLocale { get; set; } = "en_US"; + public bool Pooling { get; set; } = true; + + public string ToConnectionString() + { + // Driver name must match installed name in odbcinst.ini + var driverName = Environment.GetEnvironmentVariable("AS400_DRIVER_NAME") ?? "IBM i Access ODBC Driver"; + var parts = new List + { + $"Driver={{{driverName}}}", + $"System={System}" + }; + if (!string.IsNullOrWhiteSpace(DefaultLibraries)) parts.Add($"DefaultLibraries={DefaultLibraries}"); + if (!string.IsNullOrWhiteSpace(User)) parts.Add($"Uid={User}"); + if (!string.IsNullOrWhiteSpace(Password)) parts.Add($"Pwd={Password}"); + if (!string.IsNullOrWhiteSpace(Naming)) parts.Add($"Naming={Naming}"); + if (!string.IsNullOrWhiteSpace(Translate)) parts.Add($"TRANSLATE={Translate}"); + if (!string.IsNullOrWhiteSpace(ClientLocale)) parts.Add($"Client_Locale={ClientLocale}"); + if (Pooling) parts.Add("Pooling=true"); + return string.Join(";", parts); + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b8b18cc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,70 @@ +# ---------- build stage ---------- +FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Install tools needed to install IBM i Access ODBC (alien to convert RPM, unixODBC dev for headers) +RUN apt-get update && apt-get install -y --no-install-recommends \ + alien curl ca-certificates unzip \ + unixodbc unixodbc-dev \ + && rm -rf /var/lib/apt/lists/* + +# Ensure directories exist even if the IBM driver is absent, so later COPY succeeds +RUN mkdir -p /opt/ibm /usr/lib /usr/lib64 /tmp/odbc-libs/lib /tmp/odbc-libs/lib64 + +# Copy IBM i Access ODBC RPM from build context (put your rpm under ./drivers) +# Example filename: ibm-iaccess-1.1.0.29-1.0.x86_64.rpm.disabled (we drop the .disabled suffix below) +COPY drivers/ /tmp/drivers/ +RUN set -ex; \ + if ls /tmp/drivers/ibm-iaccess-*.rpm.disabled >/dev/null 2>&1; then \ + for f in /tmp/drivers/ibm-iaccess-*.rpm.disabled; do mv "$f" "${f%.disabled}"; done; \ + fi +RUN set -ex; \ + if ls /tmp/drivers/ibm-iaccess-*.rpm >/dev/null 2>&1; then \ + for f in /tmp/drivers/ibm-iaccess-*.rpm; do alien -i --scripts "$f"; done; \ + else echo ">> IBM i Access ODBC RPM not found in /tmp/drivers. Build will continue, but ODBC will not work until driver is present." ; fi + +# Collect IBM driver shared libraries so they can be copied without optional globs later +RUN set -ex; \ + if ls /opt/ibm/iaccess/lib64/libcwb*.so* >/dev/null 2>&1; then cp /opt/ibm/iaccess/lib64/libcwb*.so* /tmp/odbc-libs/lib64/; fi; \ + if ls /opt/ibm/iaccess/lib/libcwb*.so* >/dev/null 2>&1; then cp /opt/ibm/iaccess/lib/libcwb*.so* /tmp/odbc-libs/lib/; fi + +# Register ODBC driver (odbcinst.ini) +COPY docker/odbc/odbcinst.ini /etc/odbcinst.ini +# Optional DSN (odbc.ini) if you prefer DSN connections +COPY docker/odbc/odbc.ini /etc/odbc.ini + +# copy project and restore/build +COPY AS400API.csproj ./ +RUN dotnet restore AS400API.csproj +COPY . . +RUN dotnet publish AS400API.csproj -c Release -o /app/publish /p:UseAppHost=false + +# ---------- runtime stage ---------- +FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/aspnet:9.0 AS final +WORKDIR /app + +# Install unixODBC and copy IBM i Access ODBC libs from build stage +RUN apt-get update && apt-get install -y --no-install-recommends \ + unixodbc \ + && rm -rf /var/lib/apt/lists/* + +# Copy IBM i driver files installed in build stage (if present) +# Common path after installing ibm-iaccess via alien: +COPY --from=build /opt/ibm /opt/ibm +COPY --from=build /tmp/odbc-libs/lib64/ /usr/lib64/ +COPY --from=build /tmp/odbc-libs/lib/ /usr/lib/ + +# ODBC config +COPY docker/odbc/odbcinst.ini /etc/odbcinst.ini +COPY docker/odbc/odbc.ini /etc/odbc.ini + +# App +COPY --from=build /app/publish . + +# Default envs (override in compose) +ENV ASPNETCORE_URLS=http://+:8080 +ENV AS400_DRIVER_NAME="IBM i Access ODBC Driver" +ENV LD_LIBRARY_PATH=/opt/ibm/iaccess/lib64:/opt/ibm/iaccess/lib + +EXPOSE 8080 +ENTRYPOINT ["dotnet", "AS400API.dll"] diff --git a/Endpoints/AS400_CP3FPRD/ORDUAG.cs b/Endpoints/AS400_CP3FPRD/ORDUAG.cs new file mode 100644 index 0000000..caedd57 --- /dev/null +++ b/Endpoints/AS400_CP3FPRD/ORDUAG.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.Data.Odbc; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using AS400API.Infrastructure; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace AS400API.Endpoints; + +public static class ORDUAGEndpoint +{ + private static readonly string[] ColumnNames = + [ + "AG_CODE_COMPANY", + "AG_CODE_CURRENCY", + "AG_CODE_NUMBER", + "AGENT_AGENCY", + "AGENT_HOME_NAAD", + "AGENT_MAIL_NAAD", + "AGENT_PHONE", + "AGENT_STAT", + "AGENT_START_DATE", + "AGENT_DATE_TERM", + "AGENT_FILLER_01", + "AGENT_QUALITY_BONUS", + "AGENT_MARKET_BONUS", + "AGENT_NON_HOUSED_COMP", + "AGENT_OTHER_COMP", + "AGENT_FINANCE", + "AGENT_FILLER_02", + "AGENT_PAY_NOPAY", + "AGENT_HOLD_BACK", + "AGENT_DEVELOPMENT_AMT", + "AGENT_NON_MED_PRIV", + "AGENT_FRINGE_BENEFIT", + "AGENT_FILLER_03", + "AGENT_REGION", + "AGENT_SPONSOR_LIC", + "AGENT_PRIM_LIC_NO", + "AGENT_QUALIFICATION", + "AGENT_PREV_ID", + "AGENT_FILLER_05", + "AGENT_FYR_COMM_CURMTH", + "AGENT_FYR_COMM_YTD", + "AGENT_RENEWAL_COMM_CURMTH", + "AGENT_RENEWAL_COMM_YTD", + "AGENT_SERVFEE_CURMTH", + "AGENT_SERVFEE_YTD", + "AGENT_SINGPRM_CURMTH", + "AGENT_SINGPRM_YTD", + "AGENT_OVERRIDE_CURMTH", + "AGENT_OVERRIDE_YTD", + "AGENT_PAY_DED_CURMTH", + "AGENT_PAY_DED_YTD", + "AGENT_PAYABLE_BALANCE_YTD", + "AGENT_NET_FYR_COMM1", + "AGENT_NET_FYR_COMM2", + "AGENT_NET_FYR_COMM3", + "AGENT_NET_FYR_COMM4", + "AGENT_NET_FYR_COMM5", + "AGENT_NET_FYR_COMM6", + "AGENT_NET_FYR_COMM7", + "AGENT_NET_FYR_COMM8", + "AGENT_NET_FYR_COMM9", + "AGENT_NET_FYR_COMM10", + "AGENT_NET_FYR_COMM11", + "AGENT_NET_FYR_COMM12", + "AGENT_ASGNMT_MADE_AMT", + "AGENT_ASGNMT_REC_AMT", + "AGENT_NET_FYR_LIFE_COMM1", + "AGENT_NET_FYR_LIFE_COMM2", + "AGENT_NET_FYR_LIFE_COMM3", + "AGENT_NET_FYR_LIFE_COMM4", + "AGENT_NET_FYR_LIFE_COMM5", + "AGENT_NET_FYR_LIFE_COMM6", + "AGENT_NET_FYR_LIFE_COMM7", + "AGENT_NET_FYR_LIFE_COMM8", + "AGENT_NET_FYR_LIFE_COMM9", + "AGENT_NET_FYR_LIFE_COMM10", + "AGENT_NET_FYR_LIFE_COMM11", + "AGENT_NET_FYR_LIFE_COMM12", + "AGENT_FYC_SINGPRM_CURMTH", + "AGENT_FYC_SINGPRM_YTD", + "AGENT_FYR_LIFE_COMM_CURMTH", + "AGENT_FYR_LIFE_COMM_YTD", + "AGENT_REN_LIFE_COMM_CURMTH", + "AGENT_REN_LIFE_COMM_YTD", + "AGENT_QUALITY_BON_CURMTH", + "AGENT_QUALITY_BON_YTD", + "AGENT_MARKET_BON_CURMTH", + "AGENT_MARKET_BON_YTD", + "AGENT_NON_HOUSED_CURMTH", + "AGENT_NON_HOUSED_YTD", + "AGENT_APP_CNT_LSTMTH", + "AGENT_APP_CNT_YTM", + "AGENT_TOT_PREM_WRIT_LSTMTH", + "AGENT_TOT_PREM_WRIT_YTM", + "AGENT_LIFE_PREM_WRIT_LSTMTH", + "AGENT_LIFE_PREM_WRIT_YTM", + "AGENT_TOT_FYP_CURMTH", + "AGENT_TOT_FYP_YTD", + "AGENT_TOT_REN_CURMTH", + "AGENT_TOT_REN_YTD", + "AGENT_PERSISTENCY1", + "AGENT_PERSISTENCY2", + "AGENT_PERSISTENCY3", + "AGENT_PERSISTENCY4", + "AGENT_PRODUCTION1", + "AGENT_PRODUCTION2", + "AGENT_PRODUCTION3", + "AGENT_PRODUCTION4", + "AGENT_MDRT1", + "AGENT_MDRT2", + "AGENT_MDRT3", + "AGENT_MDRT4", + "AGENT_CONVENTIONS1", + "AGENT_CONVENTIONS2", + "AGENT_CONVENTIONS3", + "AGENT_CONVENTIONS4", + "AGENT_APPTIVITY_AWARD1", + "AGENT_APPTIVITY_AWARD2", + "AGENT_APPTIVITY_AWARD3", + "AGENT_APPTIVITY_AWARD4", + "AGENT_NQA1", + "AGENT_NQA2", + "AGENT_NQA3", + "AGENT_NQA4", + "AG_BLACKLIST_RSN", + "AG_TAX_NUMBER", + "AG_DIST_CHANEL_CODE", + "AGENT_LIAISON_OFF", + "AGENT_FILLER_07", + "AG_LIC_FPO", + "AG_LIC_START_DATE", + "AG_LIC_END_DATE", + "AG_GUARANTOR_NO", + "AG_GUARANTOR_SW", + "AG_BANK_ACCOUNT", + "AG_PREV_TAX_RATE", + "AG_CURR_TAX_RATE", + "AG_PRESERVE_AGENT", + "AG_LIC_REQ_DATE", + "AG_BLACKLIST_IND", + "AGENT_FILLER_06" + ]; + + private static readonly Regex LibraryNamePattern = new("^[A-Z0-9_]+$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static RouteGroupBuilder MapORDUAGEndpoints(this RouteGroupBuilder group) + { + group.MapGet("/v1/orduag/query", async ( + [AsParameters] OrduagQuery query, + OdbcConnection conn, + ILoggerFactory loggerFactory) => + { + var logger = loggerFactory.CreateLogger("ORDUAGEndpoint"); + try + { + await conn.OpenAsync(); + + var page = query.Page.GetValueOrDefault(1); + if (page < 1) + { + logger.LogWarning("Invalid page value {Page}; defaulting to 1", query.Page); + page = 1; + } + + var pageSize = query.PageSize.GetValueOrDefault(50); + pageSize = Math.Clamp(pageSize, 1, 500); + + var offsetLong = (long)(page - 1) * pageSize; + if (offsetLong > int.MaxValue) + { + return Results.BadRequest(new { error = "Requested page is too large." }); + } + + var offset = (int)offsetLong; + + var sqlBuilder = new StringBuilder(); + sqlBuilder.AppendLine("SELECT"); + sqlBuilder.AppendLine(string.Join(",\n", ColumnNames.Select(column => $" {column}"))); + sqlBuilder.AppendLine("FROM CP3FPRD.ORDUAG"); + sqlBuilder.AppendLine("WHERE 1 = 1"); + + var parameterValues = new List(); + + if (!string.IsNullOrWhiteSpace(query.CodeNumber)) + { + sqlBuilder.AppendLine(" AND AG_CODE_NUMBER = ?"); + parameterValues.Add(query.CodeNumber.Trim()); + } + + if (!string.IsNullOrWhiteSpace(query.AgentAgency)) + { + sqlBuilder.AppendLine(" AND AGENT_AGENCY = ?"); + parameterValues.Add(query.AgentAgency.Trim()); + } + + if (!string.IsNullOrWhiteSpace(query.AgentStat)) + { + sqlBuilder.AppendLine(" AND AGENT_STAT = ?"); + parameterValues.Add(query.AgentStat.Trim()); + } + + sqlBuilder.AppendLine("ORDER BY AG_CODE_NUMBER"); + sqlBuilder.AppendLine("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"); + + parameterValues.Add(offset); + parameterValues.Add(pageSize); + + await using var command = conn.CreateCommand(); + command.CommandText = sqlBuilder.ToString(); + + foreach (var value in parameterValues) + { + var parameter = command.CreateParameter(); + parameter.Value = value; + command.Parameters.Add(parameter); + } + + var rows = new List>(); + await using (var reader = await command.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + var row = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < reader.FieldCount; i++) + { + var value = reader.GetNormalizedValue(i); + row[reader.GetName(i)] = value!; + } + + rows.Add(row); + } + } + + logger.LogInformation( + "Fetched {Count} ORDUAG rows with filters codeNumber={CodeNumber}, agency={Agency}, status={Status}, page={Page}, pageSize={PageSize}", + rows.Count, + query.CodeNumber, + query.AgentAgency, + query.AgentStat, + page, + pageSize); + + var formatted = rows.Select(dict => dict.ToCamelCaseDictionary()).ToList(); + return Results.Ok(formatted); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to list tables for AS/400 ORDUAG"); + return Results.Problem($"Failed to list tables: {ex.Message}"); + } + }) + // .RequireAuthorization(AuthPolicies.RequireOperator) + .WithSummary("List tables for a specific AS/400 library (schema)"); + + return group; + } + + public sealed class OrduagQuery + { + public string? CodeNumber { get; init; } + + public string? AgentAgency { get; init; } + + public string? AgentStat { get; init; } + + public int? Page { get; init; } + + public int? PageSize { get; init; } + } +} diff --git a/Endpoints/As400Endpoints.cs b/Endpoints/As400Endpoints.cs new file mode 100644 index 0000000..6b531f5 --- /dev/null +++ b/Endpoints/As400Endpoints.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Data.Odbc; +using System.Linq; +using AS400API.Auth; +using AS400API.Infrastructure; +using Dapper; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; + +namespace AS400API.Endpoints; + +public static class As400Endpoints +{ + public static RouteGroupBuilder MapAs400Endpoints(this RouteGroupBuilder group) + { + group.MapGet("/v1/as400/query", async (string sql, OdbcConnection conn, ILoggerFactory loggerFactory) => + { + var logger = loggerFactory.CreateLogger("As400QueryEndpoint"); + try + { + await conn.OpenAsync(); + if (string.IsNullOrWhiteSpace(sql)) + { + logger.LogWarning("Missing sql query parameter when executing AS/400 query"); + return Results.BadRequest(new { error = "Query parameter 'sql' is required." }); + } + + logger.LogInformation("Executing AS/400 query with SQL length {Length}", sql?.Length ?? 0); + var rows = (await conn.QueryAsync(sql!)).ToList(); + var formatted = rows.ToCamelCaseDictionaries().ToList(); + logger.LogInformation("AS/400 query returned {RowCount} rows", formatted.Count); + return Results.Ok(formatted); + } + catch (Exception ex) + { + logger.LogError(ex, "AS/400 query failed"); + return Results.Problem($"Query failed: {ex.Message}"); + } + }) + .RequireAuthorization(AuthPolicies.RequireAdmin) + .WithSummary("Execute a read-only SQL select against AS/400 (admin only)") + .Produces(200) + .ProducesProblem(500); + + group.MapGet("/v1/as400/databases", async (OdbcConnection conn, ILoggerFactory loggerFactory) => + { + var logger = loggerFactory.CreateLogger("As400DatabasesEndpoint"); + try + { + await conn.OpenAsync(); + const string sqlQuery = "SELECT SCHEMA_NAME AS LIBRARY_NAME FROM QSYS2.SYSSCHEMAS ORDER BY LIBRARY_NAME"; + var schemas = (await conn.QueryAsync(sqlQuery)).ToList(); + var formatted = schemas.ToCamelCaseDictionaries().ToList(); + logger.LogInformation("Fetched {Count} AS/400 schemas", formatted.Count); + return Results.Ok(formatted); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to list AS/400 schemas"); + return Results.Problem($"Failed to list schemas: {ex.Message}"); + } + }) + .RequireAuthorization(AuthPolicies.RequireOperator) + .WithSummary("List available libraries/schemas for AS/400 accounts"); + + group.MapGet("/v1/as400/tables", async (string libraryName, OdbcConnection conn, ILoggerFactory loggerFactory) => + { + var logger = loggerFactory.CreateLogger("As400TablesEndpoint"); + try + { + await conn.OpenAsync(); + if (string.IsNullOrWhiteSpace(libraryName)) + { + logger.LogWarning("Missing libraryName query parameter when listing tables"); + return Results.BadRequest(new { error = "Query parameter 'libraryName' is required." }); + } + + const string sqlQuery = """ + SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE + FROM QSYS2.SYSTABLES + WHERE TABLE_SCHEMA = @libraryName + ORDER BY TABLE_NAME + """; + + var tables = (await conn.QueryAsync(sqlQuery, new { libraryName })).ToList(); + logger.LogInformation("Fetched {Count} tables for AS/400 library {Library}", tables.Count, libraryName); + var formatted = tables.ToCamelCaseDictionaries().ToList(); + return Results.Ok(formatted); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to list tables for AS/400 library {Library}", libraryName); + return Results.Problem($"Failed to list tables: {ex.Message}"); + } + }) + .RequireAuthorization(AuthPolicies.RequireOperator) + .WithSummary("List tables for a specific AS/400 library (schema)"); + + group.MapGet("/v1/as400/table-structure", async (string libraryName, string tableName, OdbcConnection conn, ILoggerFactory loggerFactory) => + { + var logger = loggerFactory.CreateLogger("As400TableStructureEndpoint"); + try + { + await conn.OpenAsync(); + if (string.IsNullOrWhiteSpace(libraryName) || string.IsNullOrWhiteSpace(tableName)) + { + logger.LogWarning("Missing libraryName or tableName query parameter when requesting table structure"); + return Results.BadRequest(new { error = "Query parameters 'libraryName' and 'tableName' are required." }); + } + + const string sqlQuery = """ + SELECT COLUMN_NAME, SYSTEM_COLUMN_NAME, DATA_TYPE, LENGTH, NUMERIC_SCALE, IS_NULLABLE + FROM QSYS2.SYSCOLUMNS2 + WHERE TABLE_SCHEMA = ? + AND TABLE_NAME = ? + ORDER BY ORDINAL_POSITION + """; + + await using var command = conn.CreateCommand(); + command.CommandText = sqlQuery; + + var libraryParameter = command.CreateParameter(); + libraryParameter.Value = libraryName.ToUpper(); + command.Parameters.Add(libraryParameter); + + var tableParameter = command.CreateParameter(); + tableParameter.Value = tableName.ToUpper(); + command.Parameters.Add(tableParameter); + + var columns = new List>(); + await using (var reader = await command.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + var row = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < reader.FieldCount; i++) + { + row[reader.GetName(i)] = reader.GetNormalizedValue(i)!; + } + + columns.Add(row); + } + } + + // var tables = (await conn.QueryAsync(sqlQuery, new { libraryName })).ToList(); + // var formatted = tables.ToList(); + + logger.LogInformation("Fetched {Count} columns for {Library}/{Table}", columns.Count, libraryName, tableName); + var formatted = columns.Select(dict => dict.ToCamelCaseDictionary()).ToList(); + return Results.Ok(formatted); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to list columns for {Library}/{Table}", libraryName, tableName); + return Results.Problem($"Failed to list columns: {ex.Message}"); + } + }) + // .RequireAuthorization(AuthPolicies.RequireOperator) + .WithSummary("Describe the structure of a table (columns) for an AS/400 library"); + + return group; + } +} diff --git a/Endpoints/AuthEndpoints.cs b/Endpoints/AuthEndpoints.cs new file mode 100644 index 0000000..dcdfe02 --- /dev/null +++ b/Endpoints/AuthEndpoints.cs @@ -0,0 +1,48 @@ +using System.Linq; +using System.Security.Claims; +using System.IdentityModel.Tokens.Jwt; +using AS400API.Auth; +using AS400API.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace AS400API.Endpoints; + +public static class AuthEndpoints +{ + public static RouteGroupBuilder MapAuthEndpoints(this RouteGroupBuilder group) + { + group.MapPost("/v1/auth/login", async Task (LoginRequest request, DemoUserStore userStore, TokenService tokenService, JwtOptions jwtOptions) => + { + if (request is null || string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password)) + { + return Results.BadRequest(new { error = "Username and password are required." }); + } + + var user = await userStore.FindByNameAsync(request.Username); + if (user is null || !userStore.ValidateCredentials(user, request.Password)) + { + return Results.Unauthorized(); + } + + var accessToken = tokenService.CreateToken(user); + return Results.Ok(new LoginResponse(accessToken, jwtOptions.AccessTokenLifetimeMinutes * 60, "Bearer", user.Roles)); + }) + .AllowAnonymous() + .WithSummary("Exchange credentials for a JWT access token") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized); + + group.MapGet("/v1/users/me", (ClaimsPrincipal user) => + { + var username = user.Identity?.Name ?? user.FindFirstValue(JwtRegisteredClaimNames.Sub) ?? "unknown"; + var roles = user.FindAll(ClaimTypes.Role).Select(r => r.Value).ToArray(); + return Results.Ok(new { username, roles }); + }) + .RequireAuthorization() + .WithSummary("Returns the current user's profile from the access token"); + + return group; + } +} diff --git a/Endpoints/SystemEndpoints.cs b/Endpoints/SystemEndpoints.cs new file mode 100644 index 0000000..4b8dff4 --- /dev/null +++ b/Endpoints/SystemEndpoints.cs @@ -0,0 +1,43 @@ +using System; +using System.Data.Odbc; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace AS400API.Endpoints; + +public static class SystemEndpoints +{ + public static IEndpointRouteBuilder MapRootEndpoints(this IEndpointRouteBuilder app) + { + app.MapGet("/", () => Results.Ok(new { name = "AS400API", status = "ok" })); + return app; + } + + public static RouteGroupBuilder MapSystemEndpoints(this RouteGroupBuilder group) + { + group.MapGet("/v1/health", async (OdbcConnection conn) => + { + try + { + await conn.OpenAsync(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT CURRENT_DATE AS THE_DATE FROM SYSIBM.SYSDUMMY1"; + using var reader = await cmd.ExecuteReaderAsync(); + if (await reader.ReadAsync()) + { + var d = reader.GetDateTime(0); + return Results.Ok(new { AS400 = "online", currentDateOnAS400 = d.ToString("yyyy-MM-dd"), timestamp = DateTime.UtcNow }); + } + + return Results.Ok(new { AS400 = "online", note = "no rows" }); + } + catch (Exception ex) + { + return Results.Problem($"AS400 ODBC check failed: {ex.Message}"); + } + }).AllowAnonymous(); + + return group; + } +} diff --git a/Infrastructure/DataReaderExtensions.cs b/Infrastructure/DataReaderExtensions.cs new file mode 100644 index 0000000..0ccbdbc --- /dev/null +++ b/Infrastructure/DataReaderExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.Data.Common; + +namespace AS400API.Infrastructure; + +public static class DataReaderExtensions +{ + public static object? GetNormalizedValue(this DbDataReader reader, int ordinal) + { + if (reader.IsDBNull(ordinal)) + { + return null; + } + + var value = reader.GetValue(ordinal); + + if (value is string stringValue) + { + var dataTypeName = reader.GetDataTypeName(ordinal)?.Trim(); + if (!string.IsNullOrEmpty(dataTypeName) + && dataTypeName.StartsWith("CHAR", StringComparison.OrdinalIgnoreCase) + && !dataTypeName.Contains("VARYING", StringComparison.OrdinalIgnoreCase)) + { + return stringValue.TrimEnd(); + } + } + + return value; + } +} diff --git a/Infrastructure/DatabaseRowFormatter.cs b/Infrastructure/DatabaseRowFormatter.cs new file mode 100644 index 0000000..111a1e1 --- /dev/null +++ b/Infrastructure/DatabaseRowFormatter.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace AS400API.Infrastructure; + +public static class DatabaseRowFormatter +{ + public static IEnumerable> ToCamelCaseDictionaries(this IEnumerable rows) + { + foreach (var row in rows) + { + if (row is IDictionary dict) + { + yield return dict.ToCamelCaseDictionary(); + } + else + { + yield return new Dictionary + { + ["value"] = row + }; + } + } + } + + public static IDictionary ToCamelCaseDictionary(this IDictionary source) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in source) + { + var key = string.IsNullOrWhiteSpace(kvp.Key) ? kvp.Key : ToCamelCase(kvp.Key); + result[key] = kvp.Value; + } + + return result; + } + + private static string ToCamelCase(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + var segments = input + .Split(new[] { '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.ToLowerInvariant()) + .ToArray(); + + if (segments.Length == 0) + { + return input; + } + + if (segments.Length == 1) + { + var segment = segments[0]; + if (segment.Length == 1) + { + return segment.ToLowerInvariant(); + } + + return char.ToLowerInvariant(segment[0]) + segment[1..]; + } + + var sb = new StringBuilder(); + sb.Append(segments[0]); + for (var i = 1; i < segments.Length; i++) + { + var segment = segments[i]; + if (segment.Length == 0) + { + continue; + } + + sb.Append(char.ToUpperInvariant(segment[0])); + if (segment.Length > 1) + { + sb.Append(segment[1..]); + } + } + + return sb.ToString(); + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..bae89c2 --- /dev/null +++ b/Program.cs @@ -0,0 +1,133 @@ +using System.Data.Odbc; +using System.Text; +using AS400API.Auth; +using AS400API.Configuration; +using AS400API.Endpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Serilog; + +// var builder = WebApplication.CreateBuilder(args); +var builderArgs = args ?? Array.Empty(); +var builder = WebApplication.CreateBuilder(builderArgs); + + + +builder.Host.UseSerilog((context, services, loggerConfiguration) => +{ + loggerConfiguration + .ReadFrom.Configuration(context.Configuration) + .Enrich.FromLogContext(); +}); + +// Bind ODBC settings +var odbc = new OdbcOptions(); +builder.Configuration.GetSection("Odbc").Bind(odbc); + +// Allow overriding by environment variables +odbc.System ??= Environment.GetEnvironmentVariable("AS400_SYSTEM"); +odbc.DefaultLibraries ??= Environment.GetEnvironmentVariable("AS400_DEFAULT_LIBRARIES"); +odbc.User ??= Environment.GetEnvironmentVariable("AS400_USER"); +odbc.Password ??= Environment.GetEnvironmentVariable("AS400_PASSWORD"); +odbc.Naming ??= Environment.GetEnvironmentVariable("AS400_NAMING") ?? "1"; + +if (string.IsNullOrWhiteSpace(odbc.System) || + string.IsNullOrWhiteSpace(odbc.User) || + string.IsNullOrWhiteSpace(odbc.Password)) +{ + Console.WriteLine("⚠️ Missing ODBC connection settings. Set in appsettings.json or environment variables."); +} + +// JWT configuration +var jwtOptions = builder.Configuration.GetSection(JwtOptions.SectionName).Get() ?? new JwtOptions(); +jwtOptions.EnsureIsValid(); +var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Key)); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "AS400 API", + Version = "v1" + }); + + var securityScheme = new OpenApiSecurityScheme + { + Name = "Authorization", + Description = "JWT Authorization header using the Bearer scheme.", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }; + + options.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { securityScheme, Array.Empty() } + }); +}); +builder.Services.AddSingleton(odbc); +builder.Services.AddScoped(_ => new OdbcConnection(odbc.ToConnectionString())); +builder.Services.AddSingleton(jwtOptions); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtOptions.Issuer, + ValidateAudience = true, + ValidAudience = jwtOptions.Audience, + ValidateIssuerSigningKey = true, + IssuerSigningKey = signingKey, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(1) + }; + }); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy(AuthPolicies.RequireOperator, policy => + policy.RequireRole(Roles.Admin, Roles.Operator)); + options.AddPolicy(AuthPolicies.RequireAdmin, policy => + policy.RequireRole(Roles.Admin)); +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseSerilogRequestLogging(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapRootEndpoints(); + +var api = app.MapGroup("/api"); +api.MapSystemEndpoints(); +api.MapAuthEndpoints(); +api.MapAs400Endpoints(); +api.MapORDUAGEndpoints(); + +app.Run(); + + +// env DOTNET_ENVIRONMENT=Development dotnet run +// docker run -d --name sonarqube -p 9000:9000 sonarqube:lts-community diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..10f242d --- /dev/null +++ b/appsettings.json @@ -0,0 +1,49 @@ +{ + "Odbc": { + "System": "10.200.123.68", + "DefaultLibraries": "MTDTALIB", + "User": "CAPZTMR1", + "Password": "ABC123", + "Naming": "1", + "Translate": "1", + "ClientLocale": "en_US" + }, + "Jwt": { + "Issuer": "AS400API", + "Audience": "AS400API.Clients", + "Key": "554459fdjscc22244eeeredabeer2df4576fgsa", + "AccessTokenLifetimeMinutes": 60 + }, + "Serilog": { + "Using": [ "Serilog.Sinks.File" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "Logs/as400-api-.log", + "rollingInterval": "Day", + "shared": true, + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] ({SourceContext}) {Message}{NewLine}{Exception}" + } + } + ], + "Enrich": [ "FromLogContext" ], + "Properties": { + "Application": "AS400API" + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6901f96 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +services: + api: + platform: linux/amd64 + build: + context: . + dockerfile: Dockerfile + image: as400api:dev + container_name: as400api + environment: + ASPNETCORE_ENVIRONMENT: Development + # Override credentials securely here (do NOT bake into image) + AS400_SYSTEM: "10.200.123.68" + AS400_DEFAULT_LIBRARIES: "MTDTALIB" + AS400_USER: "CAPZTMR1" + AS400_PASSWORD: "ABC123" + AS400_NAMING: "1" + AS400_DRIVER_NAME: "IBM i Access ODBC Driver" + ports: + - "8080:8080" + volumes: + - ./:/src + working_dir: /app diff --git a/docker/odbc/odbc.ini b/docker/odbc/odbc.ini new file mode 100644 index 0000000..bf4c418 --- /dev/null +++ b/docker/odbc/odbc.ini @@ -0,0 +1,8 @@ +[AS400] +Description=IBM i Access ODBC DSN +Driver=IBM i Access ODBC Driver +System=10.200.123.68 +Naming=1 +DefaultLibraries=MTDTALIB +UserID=CAPZTMR1 +Password=ABC123 \ No newline at end of file diff --git a/docker/odbc/odbcinst.ini b/docker/odbc/odbcinst.ini new file mode 100644 index 0000000..ee8f9de --- /dev/null +++ b/docker/odbc/odbcinst.ini @@ -0,0 +1,5 @@ +[IBM i Access ODBC Driver] +Description=IBM i Access ODBC Driver +Driver=/opt/ibm/iaccess/lib64/libcwbodbc.so +Threading=2 +UsageCount=1 \ No newline at end of file diff --git a/drivers/ibm-iaccess-1.1.0.28-1.0.x86_64.rpm.disabled b/drivers/ibm-iaccess-1.1.0.28-1.0.x86_64.rpm.disabled new file mode 100644 index 0000000..03034e1 Binary files /dev/null and b/drivers/ibm-iaccess-1.1.0.28-1.0.x86_64.rpm.disabled differ diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml new file mode 100644 index 0000000..5cfd613 --- /dev/null +++ b/k8s/configmap.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: as400api-config + labels: + app: as400api +data: + ASPNETCORE_ENVIRONMENT: "Production" + AS400_SYSTEM: "10.200.123.68" + AS400_DEFAULT_LIBRARIES: "MTDTALIB" + AS400_USER: "CAPZTMR1" + AS400_NAMING: "1" + AS400_DRIVER_NAME: "IBM i Access ODBC Driver" + JWT__ISSUER: "AS400API" + JWT__AUDIENCE: "AS400API.Clients" diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..03b2fc7 --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: as400api + labels: + app: as400api +spec: + replicas: 1 + selector: + matchLabels: + app: as400api + template: + metadata: + labels: + app: as400api + spec: + containers: + - name: as400api + image: as400api:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + envFrom: + - configMapRef: + name: as400api-config + - secretRef: + name: as400api-secrets + readinessProbe: + httpGet: + path: /api/v1/health + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /api/v1/health + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" diff --git a/k8s/secret-example.yaml b/k8s/secret-example.yaml new file mode 100644 index 0000000..398693b --- /dev/null +++ b/k8s/secret-example.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: as400api-secrets + labels: + app: as400api +type: Opaque +stringData: + AS400_PASSWORD: "ABC123" + JWT__KEY: "REPLACE_WITH_MIN_32_CHAR_JWT_KEY" diff --git a/k8s/secret.yaml b/k8s/secret.yaml new file mode 100644 index 0000000..398693b --- /dev/null +++ b/k8s/secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: as400api-secrets + labels: + app: as400api +type: Opaque +stringData: + AS400_PASSWORD: "ABC123" + JWT__KEY: "REPLACE_WITH_MIN_32_CHAR_JWT_KEY" diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..fa0431b --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: as400api + labels: + app: as400api +spec: + type: ClusterIP + ports: + - name: http + port: 80 + targetPort: http + selector: + app: as400api diff --git a/scripts/run-sonar.sh b/scripts/run-sonar.sh new file mode 100755 index 0000000..b1c0933 --- /dev/null +++ b/scripts/run-sonar.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +export "SONAR_TOKEN"="squ_ef2f0a2f495a32c33ed81afb16f3cdc98bf1336a" + +if [[ -z "${SONAR_HOST_URL:-}" ]]; then + echo "SONAR_HOST_URL environment variable is required" >&2 + exit 1 +fi + +if [[ -z "${SONAR_TOKEN:-}" ]]; then + echo "SONAR_TOKEN environment variable is required" >&2 + exit 1 +fi + +SONAR_PROJECT_KEY=${SONAR_PROJECT_KEY:-as400api} +SONAR_PROJECT_NAME=${SONAR_PROJECT_NAME:-AS400API} +BUILD_CONFIGURATION=${BUILD_CONFIGURATION:-Debug} +SOLUTION_FILE=${SOLUTION_FILE:-AS400API.sln} + +FIND_TEST_PROJECT=$(find . -name '*Tests.csproj' -print -quit) + +SONAR_BEGIN_ARGS=( + "/k:${SONAR_PROJECT_KEY}" + "/n:${SONAR_PROJECT_NAME}" + "/d:sonar.host.url=${SONAR_HOST_URL}" + "/d:sonar.login=${SONAR_TOKEN}" + "/d:sonar.cs.opencover.reportsPaths=**/coverage.opencover.xml" + "/d:sonar.exclusions=**/bin/**,**/obj/**" +) + +if [[ -n "${SONAR_BRANCH_NAME:-}" ]]; then + SONAR_BEGIN_ARGS+=("/d:sonar.branch.name=${SONAR_BRANCH_NAME}") +fi + +if [[ -n "${SONAR_ORGANIZATION:-}" ]]; then + SONAR_BEGIN_ARGS+=("/d:sonar.organization=${SONAR_ORGANIZATION}") +fi + +if [[ -n "${SONAR_PULL_REQUEST_KEY:-}" && -n "${SONAR_PULL_REQUEST_BRANCH:-}" && -n "${SONAR_PULL_REQUEST_BASE:-}" ]]; then + SONAR_BEGIN_ARGS+=( + "/d:sonar.pullrequest.key=${SONAR_PULL_REQUEST_KEY}" + "/d:sonar.pullrequest.branch=${SONAR_PULL_REQUEST_BRANCH}" + "/d:sonar.pullrequest.base=${SONAR_PULL_REQUEST_BASE}" + ) +fi + +begin_succeeded=0 +cleanup() { + local status=$? + if [[ ${begin_succeeded} -eq 1 ]]; then + if ! dotnet sonarscanner end "/d:sonar.login=${SONAR_TOKEN}"; then + status=$? + fi + fi + exit "${status}" +} +trap cleanup EXIT + +dotnet sonarscanner begin "${SONAR_BEGIN_ARGS[@]}" +begin_succeeded=1 + +dotnet build "${SOLUTION_FILE}" -c "${BUILD_CONFIGURATION}" + +if [[ -n "${FIND_TEST_PROJECT}" ]]; then + dotnet test "${SOLUTION_FILE}" -c "${BUILD_CONFIGURATION}" --no-build \ + /p:CollectCoverage=true \ + /p:CoverletOutputFormat=opencover \ + /p:CoverletOutput=TestResults/ +else + echo "No *Tests.csproj projects found; skipping dotnet test." >&2 +fi diff --git a/scripts/test-databases.sh b/scripts/test-databases.sh new file mode 100755 index 0000000..bfa9ee9 --- /dev/null +++ b/scripts/test-databases.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL=${BASE_URL:-http://localhost:8080} +USERNAME=${USERNAME:-admin} +PASSWORD=${PASSWORD:-Pass@123} +LOG_FILE=${LOG_FILE:-} + +pretty_json_or_raw() { + local input="$1" + if jq -e . >/dev/null 2>&1 <<<"$input"; then + jq . <<<"$input" + else + printf '%s\n' "$input" + fi +} + +timestamp() { + date '+%Y-%m-%dT%H:%M:%S%z' +} + +log() { + local message="$1" + echo "$message" >&2 + if [[ -n "$LOG_FILE" ]]; then + printf '[%s] %s\n' "$(timestamp)" "$message" >>"$LOG_FILE" + fi +} + +log_block() { + local heading="$1" + local body="$2" + if [[ -n "$LOG_FILE" ]]; then + { + printf '[%s] %s\n' "$(timestamp)" "$heading" + pretty_json_or_raw "$body" + } >>"$LOG_FILE" + fi +} + +usage() { + cat <] [--user ] [--pass ] [--log ] +You can also set environment variables: BASE_URL, USERNAME, PASSWORD, LOG_FILE. +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --url) + BASE_URL=$2 + shift 2 + ;; + --user) + USERNAME=$2 + shift 2 + ;; + --pass) + PASSWORD=$2 + shift 2 + ;; + --log) + LOG_FILE=$2 + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage + exit 1 + ;; + esac +done + +if [[ -n "$LOG_FILE" ]]; then + mkdir -p "$(dirname "$LOG_FILE")" + touch "$LOG_FILE" + log "Logging enabled: $LOG_FILE" +fi + +for tool in curl jq; do + if ! command -v "$tool" >/dev/null 2>&1; then + log "Error: $tool is required but not installed." + exit 1 + fi +done + +log "Logging in as $USERNAME against $BASE_URL..." +LOGIN_RESPONSE=$(curl -sS -X POST "$BASE_URL/api/v1/auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}") + +if ! ACCESS_TOKEN=$(printf '%s' "$LOGIN_RESPONSE" | jq -er '.accessToken'); then + log "Login failed. Response: $LOGIN_RESPONSE" + exit 1 +fi + +EXPIRES_IN=$(printf '%s' "$LOGIN_RESPONSE" | jq -r '.expiresIn // "unknown"') +ROLES=$(printf '%s' "$LOGIN_RESPONSE" | jq -r '.roles | join(", ")') + +log "Token acquired (expires in ${EXPIRES_IN}s) with roles: ${ROLES}" + +SANITIZED_LOGIN_RESPONSE=$(printf '%s' "$LOGIN_RESPONSE" | jq '.accessToken="***redacted***"') +log_block "Login response" "$SANITIZED_LOGIN_RESPONSE" + +log "Calling /api/as400/databases..." +DB_RESPONSE=$(curl -sS "$BASE_URL/api/v1/as400/databases" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Accept: application/json") + +log_block "Databases response" "$DB_RESPONSE" +pretty_json_or_raw "$DB_RESPONSE"