From b7a63892a4592e845325609d56a77bfc6cf8add7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28MarkL4YG=29?= Date: Fri, 13 Jun 2025 18:09:45 +0200 Subject: [PATCH 1/8] wip: Add OpenAPI dependency --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index 0ad924f..b1a427f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui") implementation("org.flywaydb:flyway-core") -- 2.45.3 From a886eab7a21216578dac6ef2113183b3990ed8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40Mark=2ETwoFive=29?= Date: Thu, 19 Jun 2025 19:13:28 +0200 Subject: [PATCH 2/8] api: Install automatic api-generators for frontend --- frontend/package.json | 5 +- frontend/pnpm-lock.yaml | 234 ++++++++++++++++++++++++++++++++++++- frontend/tsconfig.app.json | 1 + 3 files changed, 236 insertions(+), 4 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 4db7dd4..958b923 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,9 @@ "core-js": "^3.41.0", "roboto-fontface": "*", "vue": "^3.5.13", - "vuetify": "^3.8.1" + "vuetify": "^3.8.1", + "openapi-fetch": "^0.14.0", + "openapi-react-query": "^0.5.0" }, "devDependencies": { "@eslint/js": "^9.24.0", @@ -31,6 +33,7 @@ "eslint-plugin-vue": "^10.0.0", "msw": "^2.7.4", "npm-run-all2": "^7.0.2", + "openapi-typescript": "^7.8.0", "sass": "1.86.3", "sass-embedded": "^1.86.3", "typescript": "^5.8.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 173f269..2067bd1 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: core-js: specifier: ^3.41.0 version: 3.41.0 + openapi-fetch: + specifier: ^0.14.0 + version: 0.14.0 + openapi-react-query: + specifier: ^0.5.0 + version: 0.5.0(@tanstack/react-query@5.80.10(react@19.1.0))(openapi-fetch@0.14.0) roboto-fontface: specifier: '*' version: 0.10.0 @@ -60,6 +66,9 @@ importers: npm-run-all2: specifier: ^7.0.2 version: 7.0.2 + openapi-typescript: + specifier: ^7.8.0 + version: 7.8.0(typescript@5.8.3) sass: specifier: 1.86.3 version: 1.86.3 @@ -93,6 +102,10 @@ importers: packages: + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.25.9': resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} @@ -101,6 +114,10 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.26.2': resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} engines: {node: '>=6.0.0'} @@ -500,6 +517,16 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.2': + resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} + + '@redocly/openapi-core@1.34.3': + resolution: {integrity: sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@rollup/rollup-android-arm-eabi@4.40.0': resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} cpu: [arm] @@ -600,6 +627,14 @@ packages: cpu: [x64] os: [win32] + '@tanstack/query-core@5.80.10': + resolution: {integrity: sha512-mUNQOtzxkjL6jLbyChZoSBP6A5gQDVRUiPvW+/zw/9ftOAz+H754zCj3D8PwnzPKyHzGkQ9JbH48ukhym9LK1Q==} + + '@tanstack/react-query@5.80.10': + resolution: {integrity: sha512-6zM098J8sLy9oU60XAdzUlAH4wVzoMVsWUWiiE/Iz4fd67PplxeyL4sw/MPcVJJVhbwGGXCsHn9GrQt2mlAzig==} + peerDependencies: + react: ^18 || ^19 + '@tsconfig/node22@22.0.1': resolution: {integrity: sha512-VkgOa3n6jvs1p+r3DiwBqeEwGAwEvnVCg/hIjiANl5IEcqP3G0u5m8cBJspe1t9qjZRlZ7WFgqq5bJrGdgAKMg==} @@ -818,12 +853,20 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} alien-signals@1.0.13: resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -898,6 +941,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + character-parser@2.2.0: resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} @@ -924,6 +970,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + colorjs.io@0.5.2: resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} @@ -1191,6 +1240,10 @@ packages: headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1206,6 +1259,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + index-to-position@1.1.0: + resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==} + engines: {node: '>=18'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1243,6 +1300,13 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1257,6 +1321,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1321,6 +1388,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -1384,6 +1455,24 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + openapi-fetch@0.14.0: + resolution: {integrity: sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==} + + openapi-react-query@0.5.0: + resolution: {integrity: sha512-VtyqiamsbWsdSWtXmj/fAR+m9nNxztsof6h8ZIsjRj8c8UR/x9AIwHwd60IqwgymmFwo7qfSJQ1ZzMJrtqjQVg==} + peerDependencies: + '@tanstack/react-query': ^5.25.0 + openapi-fetch: ^0.14.0 + + openapi-typescript-helpers@0.0.15: + resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} + + openapi-typescript@7.8.0: + resolution: {integrity: sha512-1EeVWmDzi16A+siQlo/SwSGIT7HwaFAVjvMA7/jG5HMLSnrUOzPL7uSTRZZa4v/LCRxHTApHKtNY6glApEoiUQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1403,6 +1492,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -1445,6 +1538,10 @@ packages: pkg-types@2.1.0: resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} @@ -1486,6 +1583,10 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + read-package-json-fast@4.0.0: resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==} engines: {node: ^18.17.0 || >=20.5.0} @@ -1505,6 +1606,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -1706,6 +1811,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + supports-color@10.0.0: + resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1758,6 +1867,10 @@ packages: resolution: {integrity: sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ==} engines: {node: '>=16'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typescript-auto-import-cache@0.3.5: resolution: {integrity: sha512-fAIveQKsoYj55CozUiBoj4b/7WpN0i4o74wiGY5JVUEoD0XiqDk1tJqTEjgzL2/AizKQrXxyRosSebyDzBZKjw==} @@ -1829,6 +1942,9 @@ packages: resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} engines: {node: '>=4'} + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2054,6 +2170,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + yaml@2.7.1: resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} engines: {node: '>= 14'} @@ -2077,10 +2196,18 @@ packages: snapshots: + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/helper-string-parser@7.25.9': {} '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-identifier@7.27.1': {} + '@babel/parser@7.26.2': dependencies: '@babel/types': 7.26.0 @@ -2396,6 +2523,29 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.2': {} + + '@redocly/openapi-core@1.34.3(supports-color@10.0.0)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.2 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.0.0) + js-levenshtein: 1.1.6 + js-yaml: 4.1.0 + minimatch: 5.1.6 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + '@rollup/rollup-android-arm-eabi@4.40.0': optional: true @@ -2456,6 +2606,13 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.0': optional: true + '@tanstack/query-core@5.80.10': {} + + '@tanstack/react-query@5.80.10(react@19.1.0)': + dependencies: + '@tanstack/query-core': 5.80.10 + react: 19.1.0 + '@tsconfig/node22@22.0.1': {} '@types/cookie@0.6.0': {} @@ -2774,6 +2931,8 @@ snapshots: acorn@8.14.1: {} + agent-base@7.1.3: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -2783,6 +2942,8 @@ snapshots: alien-signals@1.0.13: {} + ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -2855,6 +3016,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + change-case@5.4.4: {} + character-parser@2.2.0: dependencies: is-regex: 1.2.1 @@ -2889,6 +3052,8 @@ snapshots: color-name@1.1.4: {} + colorette@1.4.0: {} + colorjs.io@0.5.2: {} concat-map@0.0.1: {} @@ -2917,9 +3082,11 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.0: + debug@4.4.0(supports-color@10.0.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.0.0 deep-is@0.1.4: {} @@ -3170,6 +3337,13 @@ snapshots: headers-polyfill@4.0.3: {} + https-proxy-agent@7.0.6(supports-color@10.0.0): + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@10.0.0) + transitivePeerDependencies: + - supports-color + ignore@5.3.2: {} immutable@5.0.2: {} @@ -3181,6 +3355,8 @@ snapshots: imurmurhash@0.1.4: {} + index-to-position@1.1.0: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -3213,6 +3389,10 @@ snapshots: isexe@3.1.1: {} + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -3223,6 +3403,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -3281,6 +3463,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 @@ -3353,6 +3539,28 @@ snapshots: object-assign@4.1.1: {} + openapi-fetch@0.14.0: + dependencies: + openapi-typescript-helpers: 0.0.15 + + openapi-react-query@0.5.0(@tanstack/react-query@5.80.10(react@19.1.0))(openapi-fetch@0.14.0): + dependencies: + '@tanstack/react-query': 5.80.10(react@19.1.0) + openapi-fetch: 0.14.0 + openapi-typescript-helpers: 0.0.15 + + openapi-typescript-helpers@0.0.15: {} + + openapi-typescript@7.8.0(typescript@5.8.3): + dependencies: + '@redocly/openapi-core': 1.34.3(supports-color@10.0.0) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.0.0 + typescript: 5.8.3 + yargs-parser: 21.1.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3376,6 +3584,12 @@ snapshots: dependencies: callsites: 3.1.0 + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.27.1 + index-to-position: 1.1.0 + type-fest: 4.41.0 + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -3408,6 +3622,8 @@ snapshots: exsolve: 1.0.4 pathe: 2.0.3 + pluralize@8.0.0: {} + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 @@ -3452,6 +3668,8 @@ snapshots: queue-microtask@1.2.3: {} + react@19.1.0: {} + read-package-json-fast@4.0.0: dependencies: json-parse-even-better-errors: 4.0.0 @@ -3467,6 +3685,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + requires-port@1.0.0: {} resolve-from@4.0.0: {} @@ -3641,6 +3861,8 @@ snapshots: strip-json-comments@3.1.1: {} + supports-color@10.0.0: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -3687,6 +3909,8 @@ snapshots: type-fest@4.31.0: {} + type-fest@4.41.0: {} + typescript-auto-import-cache@0.3.5: dependencies: semver: 7.6.3 @@ -3723,7 +3947,7 @@ snapshots: unplugin-vue-components@28.5.0(@babel/parser@7.27.0)(vue@3.5.13(typescript@5.8.3)): dependencies: chokidar: 3.6.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) local-pkg: 1.1.1 magic-string: 0.30.17 mlly: 1.7.4 @@ -3771,6 +3995,8 @@ snapshots: upath@2.0.1: {} + uri-js-replace@1.0.1: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -3915,7 +4141,7 @@ snapshots: vue-eslint-parser@10.1.3(eslint@9.24.0): dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@10.0.0) eslint: 9.24.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -3982,6 +4208,8 @@ snapshots: y18n@5.0.8: {} + yaml-ast-parser@0.0.43: {} + yaml@2.7.1: {} yargs-parser@21.1.1: {} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index e14c754..ff9139e 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -5,6 +5,7 @@ "compilerOptions": { "composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "noUncheckedIndexedAccess": true, "baseUrl": ".", "paths": { -- 2.45.3 From c462614d8d2d18417e08849243ba67b9971f8433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40Mark=2ETwoFive=29?= Date: Thu, 19 Jun 2025 20:22:07 +0200 Subject: [PATCH 3/8] core: Update Spring Boot - Also pin the Springdoc version as Gradle would not properly resolve it otherwise. --- build.gradle.kts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b1a427f..1b60828 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,8 @@ plugins { java - id("org.springframework.boot") version "3.3.5" - id("io.spring.dependency-management") version "1.1.6" - id("com.diffplug.spotless") version "6.25.0" + id("org.springframework.boot") version "3.5.0" + id("io.spring.dependency-management") version "1.1.7" + id("com.diffplug.spotless") version "7.0.4" } group = "io.github.markl4yg.hca" @@ -42,7 +42,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-web") - implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9") implementation("org.flywaydb:flyway-core") -- 2.45.3 From 5dde208e726930529cbb6f1bb321f87470932cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40Mark=2ETwoFive=29?= Date: Thu, 19 Jun 2025 20:22:41 +0200 Subject: [PATCH 4/8] feat: Enable access to OpenAPI spec and Swagger UI --- .gitignore | 5 +++- .../config/SecurityConfiguration.java | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/mlessmann/certassist/config/SecurityConfiguration.java diff --git a/.gitignore b/.gitignore index ca68435..b9175c6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ out/ ### Test files ### sqLiteDb.db -dev/ \ No newline at end of file +dev/ + +### Development settings ### +application.properties \ No newline at end of file diff --git a/src/main/java/de/mlessmann/certassist/config/SecurityConfiguration.java b/src/main/java/de/mlessmann/certassist/config/SecurityConfiguration.java new file mode 100644 index 0000000..c30d79f --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/config/SecurityConfiguration.java @@ -0,0 +1,23 @@ +package de.mlessmann.certassist.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class SecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilters(HttpSecurity http) throws Exception { + // Allow unauthenticated access to OpenAPI and swagger documentation. + // This should be removed or at least configurable at some point, but for now, this is fine (tm) + http.authorizeHttpRequests(auth -> auth + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html") + .permitAll()); + return http.build(); + } +} -- 2.45.3 From 8a843dc3001c85c9f0d78418cbca913188a4b7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40Mark=2ETwoFive=29?= Date: Thu, 19 Jun 2025 23:07:27 +0200 Subject: [PATCH 5/8] wip: Generate and configure OpenAPI spec - Create two (non-functioning) demo endpoints to check the swagger UI with - Configure Jackson to only serialize specific attributes - Configure SpringDoc so that only attributes known to Jackson are shown - Add some shortcut annotations for Json formatting --- .../config/JacksonConfiguration.java | 35 +++++++++++++ .../config/SpringdocConfiguration.java | 27 ++++++++++ .../certassist/models/Certificate.java | 35 +++++++++---- .../repositories/CertificateRepository.java | 5 +- .../certassist/web/CertificatesEndpoint.java | 51 +++++++++++++++++++ .../certassist/web/DocumentedPage.java | 29 +++++++++++ .../certassist/web/JsonIsoOffsetDate.java | 19 +++++++ 7 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 src/main/java/de/mlessmann/certassist/config/JacksonConfiguration.java create mode 100644 src/main/java/de/mlessmann/certassist/config/SpringdocConfiguration.java create mode 100644 src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java create mode 100644 src/main/java/de/mlessmann/certassist/web/DocumentedPage.java create mode 100644 src/main/java/de/mlessmann/certassist/web/JsonIsoOffsetDate.java diff --git a/src/main/java/de/mlessmann/certassist/config/JacksonConfiguration.java b/src/main/java/de/mlessmann/certassist/config/JacksonConfiguration.java new file mode 100644 index 0000000..a232b8e --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/config/JacksonConfiguration.java @@ -0,0 +1,35 @@ +package de.mlessmann.certassist.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.MapperFeature; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfiguration { + + /** + * Customizes the objectMapper so that ONLY specifically annotated fields are serialized. + * Other fields MUST NOT be serialized since they may contain sensitive information! + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer customizeObjectMapper() { + return builder -> builder + .featuresToDisable( + MapperFeature.AUTO_DETECT_FIELDS, + MapperFeature.AUTO_DETECT_GETTERS, + MapperFeature.AUTO_DETECT_IS_GETTERS + ) + .serializationInclusion(JsonInclude.Include.NON_EMPTY) + .visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) + .visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NONE) + .visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE) + .visibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE) + .visibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY) + .visibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.PUBLIC_ONLY) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/de/mlessmann/certassist/config/SpringdocConfiguration.java b/src/main/java/de/mlessmann/certassist/config/SpringdocConfiguration.java new file mode 100644 index 0000000..bf3523f --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/config/SpringdocConfiguration.java @@ -0,0 +1,27 @@ +package de.mlessmann.certassist.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.v3.core.jackson.ModelResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for Springdoc OpenAPI to respect Jackson annotations. + * This ensures that only properties that are visible to Jackson (annotated with @JsonProperty) + * are included in the OpenAPI schema. + */ +@Configuration +public class SpringdocConfiguration { + + /** + * Creates a ModelResolver that uses the same ObjectMapper as the application. + * This ensures that Springdoc respects the same visibility settings as Jackson. + * + * @param objectMapper the configured ObjectMapper from JacksonConfiguration + * @return a ModelResolver that uses the application's ObjectMapper + */ + @Bean + public ModelResolver modelResolver(ObjectMapper objectMapper) { + return new ModelResolver(objectMapper); + } +} \ No newline at end of file diff --git a/src/main/java/de/mlessmann/certassist/models/Certificate.java b/src/main/java/de/mlessmann/certassist/models/Certificate.java index fdf3d16..678facf 100644 --- a/src/main/java/de/mlessmann/certassist/models/Certificate.java +++ b/src/main/java/de/mlessmann/certassist/models/Certificate.java @@ -1,17 +1,21 @@ package de.mlessmann.certassist.models; +import com.fasterxml.jackson.annotation.JsonProperty; +import de.mlessmann.certassist.web.JsonIsoOffsetDate; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; +import lombok.*; +import org.hibernate.proxy.HibernateProxy; + import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import lombok.*; -import org.hibernate.proxy.HibernateProxy; @Entity -@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "fingerprint" }) }) +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"fingerprint"})}) @Getter @Setter @ToString @@ -25,8 +29,10 @@ public class Certificate { @NotNull @Enumerated(EnumType.STRING) + @JsonProperty private CertificateType type; + @JsonProperty private String trustingAuthority; /** @@ -39,17 +45,26 @@ public class Certificate { @Min(-1) private int requestedKeyLength; + @JsonIsoOffsetDate private OffsetDateTime notBefore; + @JsonIsoOffsetDate private OffsetDateTime notAfter; @NotNull + @JsonProperty private String subjectCommonName; + @JsonProperty private String subjectEmailAddress; + @JsonProperty private String subjectOrganization; + @JsonProperty private String subjectOrganizationalUnit; + @JsonProperty private String subjectCountry; + @JsonProperty private String subjectState; + @JsonProperty private String subjectLocality; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @@ -69,6 +84,8 @@ public class Certificate { private byte[] fullchain; @Column(nullable = false) + @JsonProperty + @Schema(description = "The certificate fingerprint. The algorithm used to derive the fingerprint is determined by OpenSSL") private String fingerprint; @Override @@ -76,11 +93,11 @@ public class Certificate { if (this == o) return true; if (o == null) return false; Class oEffectiveClass = o instanceof HibernateProxy - ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() - : o.getClass(); + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); Class thisEffectiveClass = this instanceof HibernateProxy - ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() - : this.getClass(); + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); if (thisEffectiveClass != oEffectiveClass) return false; Certificate that = (Certificate) o; return getId() != null && Objects.equals(getId(), that.getId()); @@ -89,7 +106,7 @@ public class Certificate { @Override public final int hashCode() { return this instanceof HibernateProxy - ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() - : getClass().hashCode(); + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() + : getClass().hashCode(); } } diff --git a/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java b/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java index 7976856..70a43d0 100644 --- a/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java +++ b/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java @@ -1,10 +1,11 @@ package de.mlessmann.certassist.repositories; import de.mlessmann.certassist.models.Certificate; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository -public interface CertificateRepository extends CrudRepository { +public interface CertificateRepository extends JpaRepository { + Certificate findByFingerprintIs(String fingerprint); } diff --git a/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java b/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java new file mode 100644 index 0000000..bb1b389 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java @@ -0,0 +1,51 @@ +package de.mlessmann.certassist.web; + +import de.mlessmann.certassist.models.Certificate; +import de.mlessmann.certassist.models.CertificateInfo; +import de.mlessmann.certassist.models.CertificateInfoSubject; +import de.mlessmann.certassist.repositories.CertificateRepository; +import de.mlessmann.certassist.service.CertificateCreationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class CertificatesEndpoint { + + private final CertificateRepository certificateRepository; + private final CertificateCreationService certificateCreationService; + + @GetMapping("/certificates") + @Operation(description = "Fetches certificates", responses = { + @ApiResponse(responseCode = "200", description = "Contains the returned certificates of the requested page.") + }) + public ResponseEntity> getCertificates(@ParameterObject Pageable pageable) { + var certificates = certificateRepository.findAll(pageable); + return ResponseEntity.ok(DocumentedPage.of(certificates)); + } + + @PostMapping("/certificates") + @Operation(description = "Requests a new certificate", responses = { + @ApiResponse(responseCode = "400", description = "One of the provided parameters is invalid."), + @ApiResponse(responseCode = "200", description = "Returns the newly created certificate.") + }) + public ResponseEntity createCertificate(@RequestBody CertificateInfo request) { + var createdCertificate = certificateCreationService.createCertificate( + CertificateInfo.builder() + .type(CertificateInfo.RequestType.STANDALONE_CERTIFICATE) + .issuer(CertificateInfoSubject.builder() + .commonName("Test") + ) + .build() + ); + return ResponseEntity.ok(createdCertificate); + } + +} diff --git a/src/main/java/de/mlessmann/certassist/web/DocumentedPage.java b/src/main/java/de/mlessmann/certassist/web/DocumentedPage.java new file mode 100644 index 0000000..2e73292 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/web/DocumentedPage.java @@ -0,0 +1,29 @@ +package de.mlessmann.certassist.web; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.data.domain.Page; + +import java.util.List; + +public interface DocumentedPage extends Page { + + @Override + @JsonProperty + @Schema(description = "The content of the paginated response. See nested type for more information.") + List getContent(); + + @Override + @JsonProperty("size") + @Schema(description = "How many items there are in this current page.") + int getNumberOfElements(); + + @Override + @JsonProperty + @Schema(description = "How many pages are currently available in total.") + int getTotalPages(); + + static DocumentedPage of(Page page) { + return (DocumentedPage) page; + } +} diff --git a/src/main/java/de/mlessmann/certassist/web/JsonIsoOffsetDate.java b/src/main/java/de/mlessmann/certassist/web/JsonIsoOffsetDate.java new file mode 100644 index 0000000..115661c --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/web/JsonIsoOffsetDate.java @@ -0,0 +1,19 @@ +package de.mlessmann.certassist.web; + +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.format.DateTimeFormatter; + +@JacksonAnnotationsInside +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@JsonProperty +@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") +public @interface JsonIsoOffsetDate { +} -- 2.45.3 From 532d37ce81c7ad9b74e63ea3f5619deee868fd5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40Mark=2ETwoFive=29?= Date: Sat, 21 Jun 2025 00:41:36 +0200 Subject: [PATCH 6/8] wip: Create demo endpoint for retrieving private keys --- .../certassist/web/CertificatesEndpoint.java | 37 +++++++++++++++++++ .../certassist/web/dto/PrivateKey.java | 11 ++++++ 2 files changed, 48 insertions(+) create mode 100644 src/main/java/de/mlessmann/certassist/web/dto/PrivateKey.java diff --git a/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java b/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java index bb1b389..8737584 100644 --- a/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java +++ b/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java @@ -5,20 +5,27 @@ import de.mlessmann.certassist.models.CertificateInfo; import de.mlessmann.certassist.models.CertificateInfoSubject; import de.mlessmann.certassist.repositories.CertificateRepository; import de.mlessmann.certassist.service.CertificateCreationService; +import de.mlessmann.certassist.web.dto.PrivateKey; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; +import org.springframework.util.MimeTypeUtils; import org.springframework.web.bind.annotation.*; +import java.nio.charset.StandardCharsets; + @RestController @RequestMapping("/api") @RequiredArgsConstructor(onConstructor_ = @Autowired) public class CertificatesEndpoint { + public static final String MIME_PEM_FILE = "application/x-pem-file"; private final CertificateRepository certificateRepository; private final CertificateCreationService certificateCreationService; @@ -48,4 +55,34 @@ public class CertificatesEndpoint { return ResponseEntity.ok(createdCertificate); } + @GetMapping(value = "/certificates/{cert}/privateKey", produces = { + MimeTypeUtils.APPLICATION_JSON_VALUE, + MIME_PEM_FILE + }) + @Operation(description = "Fetches the private key corresponding to the provided certificate.", + responses = { + @ApiResponse(responseCode = "200", content = { + @Content(mediaType = MimeTypeUtils.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = PrivateKey.class)), + @Content(mediaType = MIME_PEM_FILE, + schema = @Schema(type = "string", description = "PEM formatted private key content")) + }) + }) + public ResponseEntity getCertificatePrivateKey( + @RequestHeader("Accept") String acceptType, + @PathVariable String cert + ) { + var requestedCert = certificateRepository.findByFingerprintIs(cert); + + if (MimeTypeUtils.APPLICATION_JSON_VALUE.equals(acceptType)) { + String pemContent = new String(requestedCert.getPrivateKey(), StandardCharsets.UTF_8); + var pKey = new PrivateKey(pemContent); + return ResponseEntity.ok(pKey); + } else if (MIME_PEM_FILE.equals(acceptType)) { + String pemContent = new String(requestedCert.getPrivateKey(), StandardCharsets.UTF_8); + return ResponseEntity.ok(pemContent); + } else { + return ResponseEntity.badRequest().build(); + } + } } diff --git a/src/main/java/de/mlessmann/certassist/web/dto/PrivateKey.java b/src/main/java/de/mlessmann/certassist/web/dto/PrivateKey.java new file mode 100644 index 0000000..5ca46d5 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/web/dto/PrivateKey.java @@ -0,0 +1,11 @@ +package de.mlessmann.certassist.web.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Represents the private key of a certificate.") +public record PrivateKey( + @JsonProperty + @Schema(description = "The content of the private key as it would be in a .pem file.") + String pemContent) { +} -- 2.45.3 From e91bf96e743239031d4de2013243279e3054e3e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40Mark=2ETwoFive=29?= Date: Sat, 21 Jun 2025 14:06:44 +0200 Subject: [PATCH 7/8] wip: Create some frontend components to test API generation --- frontend/.gitignore | 3 ++ frontend/src/components/Home.vue | 2 +- .../src/pages/certificates/[fingerprint].vue | 34 +++++++++++++ frontend/src/pages/certificates/index.vue | 49 +++++++++++++++++++ frontend/src/pages/certificates/new.vue | 11 +++++ frontend/src/plugins/client.ts | 18 +++++++ frontend/src/plugins/index.ts | 10 ++-- frontend/typed-router.d.ts | 3 ++ .../config/SecurityConfiguration.java | 2 +- .../certassist/web/CertificatesEndpoint.java | 12 +++++ 10 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 frontend/src/pages/certificates/[fingerprint].vue create mode 100644 frontend/src/pages/certificates/index.vue create mode 100644 frontend/src/pages/certificates/new.vue create mode 100644 frontend/src/plugins/client.ts diff --git a/frontend/.gitignore b/frontend/.gitignore index 11f5d71..b5bcb0c 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -20,3 +20,6 @@ pnpm-debug.log* *.njsproj *.sln *.sw? + +# Generated source files +src/generated/ \ No newline at end of file diff --git a/frontend/src/components/Home.vue b/frontend/src/components/Home.vue index 64c98ce..4906cef 100644 --- a/frontend/src/components/Home.vue +++ b/frontend/src/components/Home.vue @@ -39,7 +39,7 @@ rounded="lg" title="Manage your certificates" variant="text" - to="/cert-request" + to="/certificates" /> diff --git a/frontend/src/pages/certificates/[fingerprint].vue b/frontend/src/pages/certificates/[fingerprint].vue new file mode 100644 index 0000000..17c9dd8 --- /dev/null +++ b/frontend/src/pages/certificates/[fingerprint].vue @@ -0,0 +1,34 @@ + + + diff --git a/frontend/src/pages/certificates/index.vue b/frontend/src/pages/certificates/index.vue new file mode 100644 index 0000000..924b91b --- /dev/null +++ b/frontend/src/pages/certificates/index.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/frontend/src/pages/certificates/new.vue b/frontend/src/pages/certificates/new.vue new file mode 100644 index 0000000..1837583 --- /dev/null +++ b/frontend/src/pages/certificates/new.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/frontend/src/plugins/client.ts b/frontend/src/plugins/client.ts new file mode 100644 index 0000000..90105c8 --- /dev/null +++ b/frontend/src/plugins/client.ts @@ -0,0 +1,18 @@ +import createFetchClient from "openapi-fetch"; +import type { components, paths } from "@/generated/api-spec.ts"; +import type { App } from "vue"; + +export const fetchClient = createFetchClient({ + baseUrl: "http://localhost:8080" +}); + +export type Schemas = components["schemas"]; + +// noinspection JSUnusedGlobalSymbols +const $api = { + install: (app: App) => { + app.config.globalProperties.$api = fetchClient; + } +} + +export default $api; diff --git a/frontend/src/plugins/index.ts b/frontend/src/plugins/index.ts index cd3a681..17430da 100644 --- a/frontend/src/plugins/index.ts +++ b/frontend/src/plugins/index.ts @@ -1,10 +1,12 @@ -import vuetify from './vuetify' -import router from '../router' +import vuetify from "./vuetify"; +import router from "../router"; +import client from "./client"; -import type { App } from 'vue' +import type { App } from "vue"; -export function registerPlugins (app: App) { +export function registerPlugins(app: App) { app .use(vuetify) .use(router) + .use(client); } diff --git a/frontend/typed-router.d.ts b/frontend/typed-router.d.ts index 8d7445f..3530519 100644 --- a/frontend/typed-router.d.ts +++ b/frontend/typed-router.d.ts @@ -20,5 +20,8 @@ declare module 'vue-router/auto-routes' { export interface RouteNamedMap { '/': RouteRecordInfo<'/', '/', Record, Record>, '/cert-request': RouteRecordInfo<'/cert-request', '/cert-request', Record, Record>, + '/certificates/': RouteRecordInfo<'/certificates/', '/certificates', Record, Record>, + '/certificates/[fingerprint]': RouteRecordInfo<'/certificates/[fingerprint]', '/certificates/:fingerprint', { fingerprint: ParamValue }, { fingerprint: ParamValue }>, + '/certificates/new': RouteRecordInfo<'/certificates/new', '/certificates/new', Record, Record>, } } diff --git a/src/main/java/de/mlessmann/certassist/config/SecurityConfiguration.java b/src/main/java/de/mlessmann/certassist/config/SecurityConfiguration.java index c30d79f..14c324a 100644 --- a/src/main/java/de/mlessmann/certassist/config/SecurityConfiguration.java +++ b/src/main/java/de/mlessmann/certassist/config/SecurityConfiguration.java @@ -16,7 +16,7 @@ public class SecurityConfiguration { // Allow unauthenticated access to OpenAPI and swagger documentation. // This should be removed or at least configurable at some point, but for now, this is fine (tm) http.authorizeHttpRequests(auth -> auth - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html") + .requestMatchers("/v3/api-docs/**", "/v3/api-docs.yaml", "/swagger-ui/**", "/swagger-ui.html") .permitAll()); return http.build(); } diff --git a/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java b/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java index 8737584..27bf6b3 100644 --- a/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java +++ b/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java @@ -38,6 +38,18 @@ public class CertificatesEndpoint { return ResponseEntity.ok(DocumentedPage.of(certificates)); } + @GetMapping("/certificates/{fingerprint}") + @Operation( + description = "Fetches a single certificate by the provided fingerprint", + responses = { + @ApiResponse(responseCode = "200") + } + ) + public ResponseEntity getCertificate(@PathVariable String fingerprint) { + var certificate = certificateRepository.findByFingerprintIs(fingerprint); + return ResponseEntity.ok(certificate); + } + @PostMapping("/certificates") @Operation(description = "Requests a new certificate", responses = { @ApiResponse(responseCode = "400", description = "One of the provided parameters is invalid."), -- 2.45.3 From b6db17e7d8dcb3589775d89b733da3155e91a245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40Mark=2ETwoFive=29?= Date: Sat, 21 Jun 2025 14:07:27 +0200 Subject: [PATCH 8/8] wip: Remove incompatible react package --- frontend/package.json | 4 ++-- frontend/pnpm-lock.yaml | 36 ------------------------------------ 2 files changed, 2 insertions(+), 38 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 958b923..e0386e7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "build": "run-p type-check \"build-only {@}\" --", "preview": "vite preview", "build-only": "vite build", + "update-types": "openapi-typescript http://localhost:8080/v3/api-docs.yaml -o src/generated/api-spec.ts", "type-check": "vue-tsc --build --force", "lint": "eslint . --fix" }, @@ -17,8 +18,7 @@ "roboto-fontface": "*", "vue": "^3.5.13", "vuetify": "^3.8.1", - "openapi-fetch": "^0.14.0", - "openapi-react-query": "^0.5.0" + "openapi-fetch": "^0.14.0" }, "devDependencies": { "@eslint/js": "^9.24.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2067bd1..3f65013 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: openapi-fetch: specifier: ^0.14.0 version: 0.14.0 - openapi-react-query: - specifier: ^0.5.0 - version: 0.5.0(@tanstack/react-query@5.80.10(react@19.1.0))(openapi-fetch@0.14.0) roboto-fontface: specifier: '*' version: 0.10.0 @@ -627,14 +624,6 @@ packages: cpu: [x64] os: [win32] - '@tanstack/query-core@5.80.10': - resolution: {integrity: sha512-mUNQOtzxkjL6jLbyChZoSBP6A5gQDVRUiPvW+/zw/9ftOAz+H754zCj3D8PwnzPKyHzGkQ9JbH48ukhym9LK1Q==} - - '@tanstack/react-query@5.80.10': - resolution: {integrity: sha512-6zM098J8sLy9oU60XAdzUlAH4wVzoMVsWUWiiE/Iz4fd67PplxeyL4sw/MPcVJJVhbwGGXCsHn9GrQt2mlAzig==} - peerDependencies: - react: ^18 || ^19 - '@tsconfig/node22@22.0.1': resolution: {integrity: sha512-VkgOa3n6jvs1p+r3DiwBqeEwGAwEvnVCg/hIjiANl5IEcqP3G0u5m8cBJspe1t9qjZRlZ7WFgqq5bJrGdgAKMg==} @@ -1458,12 +1447,6 @@ packages: openapi-fetch@0.14.0: resolution: {integrity: sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==} - openapi-react-query@0.5.0: - resolution: {integrity: sha512-VtyqiamsbWsdSWtXmj/fAR+m9nNxztsof6h8ZIsjRj8c8UR/x9AIwHwd60IqwgymmFwo7qfSJQ1ZzMJrtqjQVg==} - peerDependencies: - '@tanstack/react-query': ^5.25.0 - openapi-fetch: ^0.14.0 - openapi-typescript-helpers@0.0.15: resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} @@ -1583,10 +1566,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react@19.1.0: - resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} - engines: {node: '>=0.10.0'} - read-package-json-fast@4.0.0: resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==} engines: {node: ^18.17.0 || >=20.5.0} @@ -2606,13 +2585,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.0': optional: true - '@tanstack/query-core@5.80.10': {} - - '@tanstack/react-query@5.80.10(react@19.1.0)': - dependencies: - '@tanstack/query-core': 5.80.10 - react: 19.1.0 - '@tsconfig/node22@22.0.1': {} '@types/cookie@0.6.0': {} @@ -3543,12 +3515,6 @@ snapshots: dependencies: openapi-typescript-helpers: 0.0.15 - openapi-react-query@0.5.0(@tanstack/react-query@5.80.10(react@19.1.0))(openapi-fetch@0.14.0): - dependencies: - '@tanstack/react-query': 5.80.10(react@19.1.0) - openapi-fetch: 0.14.0 - openapi-typescript-helpers: 0.0.15 - openapi-typescript-helpers@0.0.15: {} openapi-typescript@7.8.0(typescript@5.8.3): @@ -3668,8 +3634,6 @@ snapshots: queue-microtask@1.2.3: {} - react@19.1.0: {} - read-package-json-fast@4.0.0: dependencies: json-parse-even-better-errors: 4.0.0 -- 2.45.3