diff --git a/.gitignore b/.gitignore index f22d021..e6d080a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ out/ generated/ .planning/ + diff --git a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java index ff37f32..6a218ca 100644 --- a/common/src/main/java/com/shipflow/common/domain/BaseEntity.java +++ b/common/src/main/java/com/shipflow/common/domain/BaseEntity.java @@ -41,7 +41,7 @@ public abstract class BaseEntity { protected LocalDateTime deletedAt; protected UUID deletedBy; - + protected void softDelete(UUID userId) { this.deletedAt = LocalDateTime.now(); this.deletedBy = userId; diff --git a/docker-compose.yml b/docker-compose.yml index 984794f..648c753 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: - shipflow_postgres_data:/var/lib/postgresql/data - ./docker/postgres/init:/docker-entrypoint-initdb.d healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}" ] interval: 10s timeout: 5s retries: 5 @@ -43,7 +43,7 @@ services: ports: - "6379:6379" healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: [ "CMD", "redis-cli", "ping" ] interval: 10s timeout: 5s retries: 10 @@ -59,7 +59,7 @@ services: ports: - "8761:8761" healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8761/actuator/health"] + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8761/actuator/health" ] interval: 10s timeout: 5s retries: 5 @@ -113,9 +113,15 @@ services: args: MODULE: notification-service ports: - - "8080" + - "8087:8080" environment: <<: *service-env + SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN} + GEMINI_API_KEY: ${GEMINI_API_KEY} + RABBITMQ_HOST: rabbitmq + RABBITMQ_PORT: 5672 + RABBITMQ_USERNAME: ${RABBITMQ_USERNAME} + RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD} depends_on: <<: *service-depends-on @@ -183,7 +189,7 @@ services: volumes: - shipflow_rabbitmq_data:/var/lib/rabbitmq healthcheck: - test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + test: [ "CMD", "rabbitmq-diagnostics", "-q", "ping" ] interval: 10s timeout: 5s retries: 10 diff --git a/keycloak/shipflow-export.json b/keycloak/shipflow-export.json index 0f59194..64d232f 100644 --- a/keycloak/shipflow-export.json +++ b/keycloak/shipflow-export.json @@ -1,1799 +1,2143 @@ { - "id" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "realm" : "shipflow", - "notBefore" : 0, - "defaultSignatureAlgorithm" : "RS256", - "revokeRefreshToken" : false, - "refreshTokenMaxReuse" : 0, - "accessTokenLifespan" : 300, - "accessTokenLifespanForImplicitFlow" : 900, - "ssoSessionIdleTimeout" : 1800, - "ssoSessionMaxLifespan" : 36000, - "ssoSessionIdleTimeoutRememberMe" : 0, - "ssoSessionMaxLifespanRememberMe" : 0, - "offlineSessionIdleTimeout" : 2592000, - "offlineSessionMaxLifespanEnabled" : false, - "offlineSessionMaxLifespan" : 5184000, - "clientSessionIdleTimeout" : 0, - "clientSessionMaxLifespan" : 0, - "clientOfflineSessionIdleTimeout" : 0, - "clientOfflineSessionMaxLifespan" : 0, - "accessCodeLifespan" : 60, - "accessCodeLifespanUserAction" : 300, - "accessCodeLifespanLogin" : 1800, - "actionTokenGeneratedByAdminLifespan" : 43200, - "actionTokenGeneratedByUserLifespan" : 300, - "oauth2DeviceCodeLifespan" : 600, - "oauth2DevicePollingInterval" : 5, - "enabled" : true, - "sslRequired" : "external", - "registrationAllowed" : false, - "registrationEmailAsUsername" : false, - "rememberMe" : false, - "verifyEmail" : false, - "loginWithEmailAllowed" : false, - "duplicateEmailsAllowed" : false, - "resetPasswordAllowed" : false, - "editUsernameAllowed" : false, - "bruteForceProtected" : false, - "permanentLockout" : false, - "maxTemporaryLockouts" : 0, - "maxFailureWaitSeconds" : 900, - "minimumQuickLoginWaitSeconds" : 60, - "waitIncrementSeconds" : 60, - "quickLoginCheckMilliSeconds" : 1000, - "maxDeltaTimeSeconds" : 43200, - "failureFactor" : 30, - "roles" : { - "realm" : [ { - "id" : "9c6ebe92-6809-44ff-b785-61ca2ec674a5", - "name" : "COMPANY_MANAGER", - "description" : "", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "437b33b6-c49b-4cd6-9e1e-de118ba010ff", - "name" : "HUB_MANAGER", - "description" : "", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "dbea48c0-3ebf-49e9-acb1-60036fc05185", - "name" : "MASTER", - "description" : "", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", - "name" : "default-roles-shipflow", - "description" : "${role_default-roles}", - "composite" : false, - "composites" : { - "realm" : [ "offline_access", "uma_authorization" ], - "client" : { - "account" : [ "manage-account", "view-profile" ] + "id": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "realm": "shipflow", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": false, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "9c6ebe92-6809-44ff-b785-61ca2ec674a5", + "name": "COMPANY_MANAGER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "437b33b6-c49b-4cd6-9e1e-de118ba010ff", + "name": "HUB_MANAGER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "dbea48c0-3ebf-49e9-acb1-60036fc05185", + "name": "MASTER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", + "name": "default-roles-shipflow", + "description": "${role_default-roles}", + "composite": false, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "manage-account", + "view-profile" + ] + } + }, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "d17dde63-adeb-41df-838d-c8ee55622c70", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "febb44ff-b3c0-4870-a9e8-d84afb05bc51", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + }, + { + "id": "d1077de8-c2e4-4ac7-8918-09e7e5d20fce", + "name": "SHIPMENT_MANAGER", + "description": "", + "composite": false, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "5ac95120-9c91-42fc-a9df-6e38970dad79", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "caa610f3-4986-42d7-b720-6bd8132da76a", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "9e5a736d-34b7-4aac-b8ac-7cd14650f379", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "812427c2-0481-4ba6-9e67-ee98739d7489", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "bf3899b3-2a07-49fc-b3cf-20537e4b7d5c", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "8c3c30fe-95a8-413e-85b0-0f963e5a643c", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "0d5b79cd-b3cc-4102-9811-9ee2ddd217c5", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "0d7742bd-b6dc-4ca6-ab61-3f516d540624", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "cab6e153-818c-467f-9bb2-652120c2fe4d", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ebb9d35e-deb8-4b59-86df-0cef09d4640c", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ffbcbee0-7c65-4d6e-a179-7185ddf707c1", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "5237356a-5e32-4ddb-bac7-4e521a56326e", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ce72a656-73d8-49c9-a04e-cfefd0e89d13", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "c5fa6724-d145-484d-8f33-dafa65b13b74", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "d5324b44-3656-4978-bb13-96915cdf60ff", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "ef01e291-c2c5-4d47-bc83-e50a1971ee55", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "34d178dc-ed4b-4517-b261-de55f93c7ab9", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "de1f054b-15ad-4839-a54e-537039a31820", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-realm", + "view-clients", + "manage-clients", + "query-clients", + "view-authorization", + "view-realm", + "manage-events", + "create-client", + "query-realms", + "view-events", + "view-identity-providers", + "view-users", + "query-users", + "manage-authorization", + "manage-identity-providers", + "impersonation", + "manage-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} + }, + { + "id": "5df84e42-5988-4b59-a8eb-b83835f1f662", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "attributes": {} } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "d66bcb04-700d-483c-9628-207d0bd431bd", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "0b06878c-6957-40d0-8783-23e075fcf103", + "attributes": {} + } + ], + "account": [ + { + "id": "044b18e6-b560-43c8-925d-80fbce6ddc28", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "fa040d5d-ca2f-4386-85bd-75716818b4a0", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "76c4a106-bd5d-43b2-9098-708ea5813f2a", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "c99f7465-dd93-4f8d-ab26-64439d75f0c2", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "43c296b2-6d12-41f8-898a-2663dde41447", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "69d5a2a9-fece-4c89-9faa-093ad57d9cf5", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "b48d559d-9f77-4106-afef-8d9ba7c62e2f", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + }, + { + "id": "49dce239-8341-44bf-8a1b-09b9992da150", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", + "name": "default-roles-shipflow", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "d12620b4-0367-4175-aad4-51e7d6f31fae" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "users": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "3e1fcbc4-434d-4104-86b2-206948dae66c", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/shipflow/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/shipflow/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "d944f492-235f-4a2e-8713-5f4893969dcb", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/shipflow/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/shipflow/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" }, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "d17dde63-adeb-41df-838d-c8ee55622c70", - "name" : "uma_authorization", - "description" : "${role_uma_authorization}", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "febb44ff-b3c0-4870-a9e8-d84afb05bc51", - "name" : "offline_access", - "description" : "${role_offline-access}", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - }, { - "id" : "d1077de8-c2e4-4ac7-8918-09e7e5d20fce", - "name" : "SHIPMENT_MANAGER", - "description" : "", - "composite" : false, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae", - "attributes" : { } - } ], - "client" : { - "realm-management" : [ { - "id" : "5ac95120-9c91-42fc-a9df-6e38970dad79", - "name" : "manage-realm", - "description" : "${role_manage-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "caa610f3-4986-42d7-b720-6bd8132da76a", - "name" : "manage-clients", - "description" : "${role_manage-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "9e5a736d-34b7-4aac-b8ac-7cd14650f379", - "name" : "query-clients", - "description" : "${role_query-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "812427c2-0481-4ba6-9e67-ee98739d7489", - "name" : "view-clients", - "description" : "${role_view-clients}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "query-clients" ] + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "b5c354ab-6b26-437a-960b-3aacde70b086", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "baa30b35-5489-4baf-85e0-a05059153a81", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "0b06878c-6957-40d0-8783-23e075fcf103", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "f1683ac5-4c05-4465-8c45-351b7a533da9", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "clientId": "shipflow-api", + "name": "${login-client-id}", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "login_theme": "", + "display.on.consent.screen": "false", + "consent.screen.text": "", + "frontchannel.logout.url": "", + "backchannel.logout.url": "" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + }, + "authorizationServicesEnabled": false + }, + { + "id": "39987a2a-1607-481c-a891-e33a14c9c337", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/shipflow/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/shipflow/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "9e3dc333-40f0-457e-b5e7-fdadd440aa78", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "80461e14-093c-4647-b9fb-b7a8fc843ff2", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "abdac8d3-83eb-4993-9c0a-57221ffc55b4", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "33d17aeb-10a0-4f8c-9e6c-afdf595da401", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "17861556-524a-4d33-9e30-d7df5297f61e", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${profileScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "c88cf316-27ec-4940-9b87-614a6125ee3a", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "828bd040-4877-452e-b083-3a658a52853c", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "341a7f66-f7fc-4cac-8584-77dc6444e259", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "6415f321-b218-401d-88c0-492b428cd86c", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "b971d53b-9925-4ffe-a649-4aab9ed3d880", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "09e729d0-d8dc-4693-a693-37d3237aadcd", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "8123f18d-17b8-454e-9eb9-493e3ecf988b", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "041d5689-35c8-4776-ba95-96ca02ac2b2f", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "e800e5a6-90b5-4987-944e-5226110af8cf", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "26f011c7-5681-4e8e-8f1a-d171495b2639", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" } }, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "bf3899b3-2a07-49fc-b3cf-20537e4b7d5c", - "name" : "view-authorization", - "description" : "${role_view-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "8c3c30fe-95a8-413e-85b0-0f963e5a643c", - "name" : "manage-events", - "description" : "${role_manage-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "0d5b79cd-b3cc-4102-9811-9ee2ddd217c5", - "name" : "view-realm", - "description" : "${role_view-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "0d7742bd-b6dc-4ca6-ab61-3f516d540624", - "name" : "create-client", - "description" : "${role_create-client}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "cab6e153-818c-467f-9bb2-652120c2fe4d", - "name" : "query-realms", - "description" : "${role_query-realms}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "ebb9d35e-deb8-4b59-86df-0cef09d4640c", - "name" : "view-events", - "description" : "${role_view-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "ffbcbee0-7c65-4d6e-a179-7185ddf707c1", - "name" : "view-identity-providers", - "description" : "${role_view-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "5237356a-5e32-4ddb-bac7-4e521a56326e", - "name" : "query-users", - "description" : "${role_query-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "ce72a656-73d8-49c9-a04e-cfefd0e89d13", - "name" : "view-users", - "description" : "${role_view-users}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "query-users", "query-groups" ] + { + "id": "dde63b52-45c9-4587-9341-b5e5fe4e75a2", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" } }, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "c5fa6724-d145-484d-8f33-dafa65b13b74", - "name" : "manage-authorization", - "description" : "${role_manage-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "d5324b44-3656-4978-bb13-96915cdf60ff", - "name" : "impersonation", - "description" : "${role_impersonation}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "ef01e291-c2c5-4d47-bc83-e50a1971ee55", - "name" : "manage-identity-providers", - "description" : "${role_manage-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "34d178dc-ed4b-4517-b261-de55f93c7ab9", - "name" : "manage-users", - "description" : "${role_manage-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "de1f054b-15ad-4839-a54e-537039a31820", - "name" : "realm-admin", - "description" : "${role_realm-admin}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "manage-realm", "view-clients", "manage-clients", "query-clients", "view-authorization", "view-realm", "manage-events", "create-client", "query-realms", "view-events", "view-identity-providers", "view-users", "query-users", "manage-authorization", "manage-identity-providers", "impersonation", "manage-users", "query-groups" ] + { + "id": "f941894d-e133-44ee-8747-698c9b3ffa76", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" } }, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - }, { - "id" : "5df84e42-5988-4b59-a8eb-b83835f1f662", - "name" : "query-groups", - "description" : "${role_query-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "attributes" : { } - } ], - "security-admin-console" : [ ], - "admin-cli" : [ ], - "account-console" : [ ], - "broker" : [ { - "id" : "d66bcb04-700d-483c-9628-207d0bd431bd", - "name" : "read-token", - "description" : "${role_read-token}", - "composite" : false, - "clientRole" : true, - "containerId" : "0b06878c-6957-40d0-8783-23e075fcf103", - "attributes" : { } - } ], - "account" : [ { - "id" : "044b18e6-b560-43c8-925d-80fbce6ddc28", - "name" : "manage-consent", - "description" : "${role_manage-consent}", - "composite" : true, - "composites" : { - "client" : { - "account" : [ "view-consent" ] + { + "id": "d15e02e3-4763-4519-a622-365a2840d37f", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "37b732b1-7c6d-43ed-ac23-ab884a51b1f2", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "3d56cee0-2f92-4161-b112-affc02423932", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${phoneScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "18102df6-716f-4670-b62e-f7c072e3b8c5", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": true, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "09913ba8-d796-4445-ad45-e8c13d6b3f6e", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "ab9eb324-487d-44a3-9a61-d05f989bc13b", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "e7480737-d23a-4057-ac0b-4527d5747338", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "148965f8-21a6-4eb0-a062-3c7138f2c351", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "674690ff-249d-434e-8c01-cdc901c02360", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "b40f77db-de92-4c75-b79b-10506658a17f", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "e3ce3daa-b344-4d67-8ff8-374157a39321", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "consent.screen.text": "${rolesScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "e82a1e72-897a-4b29-9138-6047db2d2d55", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "677bda21-6981-4dff-ab3d-2d17011fa95f", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "81d25232-91dc-4ceb-903d-46b2d0953ea2", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "e1876d7a-6014-4b10-a5cb-caadbd6e93fc", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${addressScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "a44b439c-7386-4fee-849f-d7bbc0ce44ba", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "d2f34d78-2c96-436e-ac6b-05f34a46de36", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "fea6c421-3e54-499f-9189-38d992452d28", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "8ba117c5-dcf2-43f8-9e37-bc2971e1acea", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "consent.screen.text": "${emailScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "4d4e2188-d8b7-44c1-b16d-3ddf2f4ff3ad", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" } }, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "fa040d5d-ca2f-4386-85bd-75716818b4a0", - "name" : "manage-account-links", - "description" : "${role_manage-account-links}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "76c4a106-bd5d-43b2-9098-708ea5813f2a", - "name" : "delete-account", - "description" : "${role_delete-account}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "c99f7465-dd93-4f8d-ab26-64439d75f0c2", - "name" : "view-applications", - "description" : "${role_view-applications}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "43c296b2-6d12-41f8-898a-2663dde41447", - "name" : "view-consent", - "description" : "${role_view-consent}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "69d5a2a9-fece-4c89-9faa-093ad57d9cf5", - "name" : "manage-account", - "description" : "${role_manage-account}", - "composite" : true, - "composites" : { - "client" : { - "account" : [ "manage-account-links" ] + { + "id": "9394eaaf-125a-4df9-93d4-c7a309a81606", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "b21f5c51-8398-4dfa-ad0b-9b57c488a3e4", + "name": "basic", + "description": "OpenID Connect scope for add all basic claims to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "4cb5d24c-3f20-4e05-b918-feb5b7fbe7bb", + "name": "auth_time", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "AUTH_TIME", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "auth_time", + "jsonType.label": "long" } }, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "b48d559d-9f77-4106-afef-8d9ba7c62e2f", - "name" : "view-groups", - "description" : "${role_view-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - }, { - "id" : "49dce239-8341-44bf-8a1b-09b9992da150", - "name" : "view-profile", - "description" : "${role_view-profile}", - "composite" : false, - "clientRole" : true, - "containerId" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "attributes" : { } - } ] + { + "id": "d067706a-9d23-456b-acb1-253f62f663c5", + "name": "sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-sub-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr", + "basic" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" }, - "groups" : [ ], - "defaultRole" : { - "id" : "b17f2fc6-8d99-4dd4-8119-9a1b09635ee1", - "name" : "default-roles-shipflow", - "description" : "${role_default-roles}", - "composite" : true, - "clientRole" : false, - "containerId" : "d12620b4-0367-4175-aad4-51e7d6f31fae" - }, - "requiredCredentials" : [ "password" ], - "otpPolicyType" : "totp", - "otpPolicyAlgorithm" : "HmacSHA1", - "otpPolicyInitialCounter" : 0, - "otpPolicyDigits" : 6, - "otpPolicyLookAheadWindow" : 1, - "otpPolicyPeriod" : 30, - "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], - "localizationTexts" : { }, - "webAuthnPolicyRpEntityName" : "keycloak", - "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], - "webAuthnPolicyRpId" : "", - "webAuthnPolicyAttestationConveyancePreference" : "not specified", - "webAuthnPolicyAuthenticatorAttachment" : "not specified", - "webAuthnPolicyRequireResidentKey" : "not specified", - "webAuthnPolicyUserVerificationRequirement" : "not specified", - "webAuthnPolicyCreateTimeout" : 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyAcceptableAaguids" : [ ], - "webAuthnPolicyExtraOrigins" : [ ], - "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], - "webAuthnPolicyPasswordlessRpId" : "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", - "webAuthnPolicyPasswordlessCreateTimeout" : 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], - "webAuthnPolicyPasswordlessExtraOrigins" : [ ], - "users" : [], - "scopeMappings" : [ { - "clientScope" : "offline_access", - "roles" : [ "offline_access" ] - } ], - "clientScopeMappings" : { - "account" : [ { - "client" : "account-console", - "roles" : [ "manage-account", "view-groups" ] - } ] - }, - "clients" : [ { - "id" : "3e1fcbc4-434d-4104-86b2-206948dae66c", - "clientId" : "account", - "name" : "${client_account}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/shipflow/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/shipflow/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "fda8b12c-9bb9-4c04-860a-5b69202d5714", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "d944f492-235f-4a2e-8713-5f4893969dcb", - "clientId" : "account-console", - "name" : "${client_account-console}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/shipflow/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/shipflow/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" + { + "id": "ff955351-02a7-4978-9dc8-3a7c3e46fb91", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "b5c354ab-6b26-437a-960b-3aacde70b086", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "baa30b35-5489-4baf-85e0-a05059153a81", - "clientId" : "admin-cli", - "name" : "${client_admin-cli}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : false, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "0b06878c-6957-40d0-8783-23e075fcf103", - "clientId" : "broker", - "name" : "${client_broker}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "f1683ac5-4c05-4465-8c45-351b7a533da9", - "clientId" : "realm-management", - "name" : "${client_realm-management}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "clientId": "shipflow-api", - "name": "${login-client-id}", - "description": "", - "rootUrl": "", - "adminUrl": "", - "baseUrl": "", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "/*" - ], - "webOrigins": [ - "/*" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": true, - "protocol": "openid-connect", - "attributes": { - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "true", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "login_theme": "", - "display.on.consent.screen": "false", - "consent.screen.text": "", - "frontchannel.logout.url": "", - "backchannel.logout.url": "" + { + "id": "f778c69e-8c53-427c-a299-5d49906aea63", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": [ - "web-origins", - "acr", - "profile", - "roles", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ], - "access": { - "view": true, - "configure": true, - "manage": true + { + "id": "d3d5c34e-35cd-49aa-8e03-90dce67080ee", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] }, - "authorizationServicesEnabled": false - },{ - "id" : "39987a2a-1607-481c-a891-e33a14c9c337", - "clientId" : "security-admin-console", - "name" : "${client_security-admin-console}", - "rootUrl" : "${authAdminUrl}", - "baseUrl" : "/admin/shipflow/console/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/admin/shipflow/console/*" ], - "webOrigins" : [ "+" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" + { + "id": "49d5fa70-ee56-4b5b-8383-05c1a1ca8cc4", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "9e3dc333-40f0-457e-b5e7-fdadd440aa78", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - } ], - "clientScopes" : [ { - "id" : "80461e14-093c-4647-b9fb-b7a8fc843ff2", - "name" : "microprofile-jwt", - "description" : "Microprofile - JWT built-in scope", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "false" + { + "id": "9eaf459c-709f-46d7-b312-bb3d01bf561e", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "abdac8d3-83eb-4993-9c0a-57221ffc55b4", - "name" : "upn", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "upn", - "jsonType.label" : "String" - } - }, { - "id" : "33d17aeb-10a0-4f8c-9e6c-afdf595da401", - "name" : "groups", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "multivalued" : "true", - "user.attribute" : "foo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "groups", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "17861556-524a-4d33-9e30-d7df5297f61e", - "name" : "profile", - "description" : "OpenID Connect built-in scope: profile", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${profileScopeConsentText}", - "display.on.consent.screen" : "true" + { + "id": "8a6ccaa6-96a0-45a7-af86-69238b8762ea", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "c88cf316-27ec-4940-9b87-614a6125ee3a", - "name" : "website", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "website", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "website", - "jsonType.label" : "String" - } - }, { - "id" : "828bd040-4877-452e-b083-3a658a52853c", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - }, { - "id" : "341a7f66-f7fc-4cac-8584-77dc6444e259", - "name" : "updated at", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "updatedAt", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "updated_at", - "jsonType.label" : "long" - } - }, { - "id" : "6415f321-b218-401d-88c0-492b428cd86c", - "name" : "full name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-full-name-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "userinfo.token.claim" : "true" - } - }, { - "id" : "b971d53b-9925-4ffe-a649-4aab9ed3d880", - "name" : "given name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "firstName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "given_name", - "jsonType.label" : "String" - } - }, { - "id" : "09e729d0-d8dc-4693-a693-37d3237aadcd", - "name" : "picture", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "picture", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "picture", - "jsonType.label" : "String" - } - }, { - "id" : "8123f18d-17b8-454e-9eb9-493e3ecf988b", - "name" : "username", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "preferred_username", - "jsonType.label" : "String" - } - }, { - "id" : "041d5689-35c8-4776-ba95-96ca02ac2b2f", - "name" : "family name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "lastName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "family_name", - "jsonType.label" : "String" - } - }, { - "id" : "e800e5a6-90b5-4987-944e-5226110af8cf", - "name" : "middle name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "middleName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "middle_name", - "jsonType.label" : "String" - } - }, { - "id" : "26f011c7-5681-4e8e-8f1a-d171495b2639", - "name" : "gender", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "gender", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "gender", - "jsonType.label" : "String" - } - }, { - "id" : "dde63b52-45c9-4587-9341-b5e5fe4e75a2", - "name" : "birthdate", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "birthdate", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "birthdate", - "jsonType.label" : "String" - } - }, { - "id" : "f941894d-e133-44ee-8747-698c9b3ffa76", - "name" : "zoneinfo", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "zoneinfo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "zoneinfo", - "jsonType.label" : "String" - } - }, { - "id" : "d15e02e3-4763-4519-a622-365a2840d37f", - "name" : "profile", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "profile", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "profile", - "jsonType.label" : "String" - } - }, { - "id" : "37b732b1-7c6d-43ed-ac23-ab884a51b1f2", - "name" : "nickname", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "nickname", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "nickname", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "3d56cee0-2f92-4161-b112-affc02423932", - "name" : "phone", - "description" : "OpenID Connect built-in scope: phone", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${phoneScopeConsentText}", - "display.on.consent.screen" : "true" + { + "id": "a726d5c4-022e-475b-bf0d-57e9dcb911fa", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "18102df6-716f-4670-b62e-f7c072e3b8c5", - "name" : "phone number verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : true, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumberVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number_verified", - "jsonType.label" : "boolean" - } - }, { - "id" : "09913ba8-d796-4445-ad45-e8c13d6b3f6e", - "name" : "phone number", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumber", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "ab9eb324-487d-44a3-9a61-d05f989bc13b", - "name" : "web-origins", - "description" : "OpenID Connect scope for add allowed web origins to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "consent.screen.text" : "", - "display.on.consent.screen" : "false" + { + "id": "86523e55-21bf-44c9-b107-6f853f0f168a", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "e7480737-d23a-4057-ac0b-4527d5747338", - "name" : "allowed web origins", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-allowed-origins-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" - } - } ] - }, { - "id" : "148965f8-21a6-4eb0-a062-3c7138f2c351", - "name" : "acr", - "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false" + { + "id": "2a4f0f8e-5e32-44b9-b820-4191fb1d4ffc", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "674690ff-249d-434e-8c01-cdc901c02360", - "name" : "acr loa level", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-acr-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true" - } - } ] - }, { - "id" : "b40f77db-de92-4c75-b79b-10506658a17f", - "name" : "offline_access", - "description" : "OpenID Connect built-in scope: offline_access", - "protocol" : "openid-connect", - "attributes" : { - "consent.screen.text" : "${offlineAccessScopeConsentText}", - "display.on.consent.screen" : "true" - } - }, { - "id" : "e3ce3daa-b344-4d67-8ff8-374157a39321", - "name" : "roles", - "description" : "OpenID Connect scope for add user roles to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "consent.screen.text" : "${rolesScopeConsentText}", - "display.on.consent.screen" : "true" + { + "id": "afd90c4f-9ed8-4ba5-9cec-1f05cabf610a", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "e82a1e72-897a-4b29-9138-6047db2d2d55", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" - } - }, { - "id" : "677bda21-6981-4dff-ab3d-2d17011fa95f", - "name" : "realm roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "foo", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "realm_access.roles", - "jsonType.label" : "String", - "multivalued" : "true" - } - }, { - "id" : "81d25232-91dc-4ceb-903d-46b2d0953ea2", - "name" : "client roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-client-role-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "foo", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "resource_access.${client_id}.roles", - "jsonType.label" : "String", - "multivalued" : "true" - } - } ] - }, { - "id" : "e1876d7a-6014-4b10-a5cb-caadbd6e93fc", - "name" : "address", - "description" : "OpenID Connect built-in scope: address", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${addressScopeConsentText}", - "display.on.consent.screen" : "true" + { + "id": "ceac3589-19df-47f7-ad07-2077ea1fc5d4", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "a44b439c-7386-4fee-849f-d7bbc0ce44ba", - "name" : "address", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-address-mapper", - "consentRequired" : false, - "config" : { - "user.attribute.formatted" : "formatted", - "user.attribute.country" : "country", - "introspection.token.claim" : "true", - "user.attribute.postal_code" : "postal_code", - "userinfo.token.claim" : "true", - "user.attribute.street" : "street", - "id.token.claim" : "true", - "user.attribute.region" : "region", - "access.token.claim" : "true", - "user.attribute.locality" : "locality" - } - } ] - }, { - "id" : "d2f34d78-2c96-436e-ac6b-05f34a46de36", - "name" : "role_list", - "description" : "SAML role list", - "protocol" : "saml", - "attributes" : { - "consent.screen.text" : "${samlRoleListScopeConsentText}", - "display.on.consent.screen" : "true" + { + "id": "ba752550-cfc6-48b3-a521-4972685f42b2", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "fea6c421-3e54-499f-9189-38d992452d28", - "name" : "role list", - "protocol" : "saml", - "protocolMapper" : "saml-role-list-mapper", - "consentRequired" : false, - "config" : { - "single" : "false", - "attribute.nameformat" : "Basic", - "attribute.name" : "Role" - } - } ] - }, { - "id" : "8ba117c5-dcf2-43f8-9e37-bc2971e1acea", - "name" : "email", - "description" : "OpenID Connect built-in scope: email", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${emailScopeConsentText}", - "display.on.consent.screen" : "true" + { + "id": "ed913470-af37-429c-af89-cc1397fb4147", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "4d4e2188-d8b7-44c1-b16d-3ddf2f4ff3ad", - "name" : "email verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "emailVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email_verified", - "jsonType.label" : "boolean" - } - }, { - "id" : "9394eaaf-125a-4df9-93d4-c7a309a81606", - "name" : "email", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "email", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "b21f5c51-8398-4dfa-ad0b-9b57c488a3e4", - "name" : "basic", - "description" : "OpenID Connect scope for add all basic claims to the token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false" + { + "id": "295facf5-3aca-4b02-a0cf-360593092a46", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "514b1696-d644-4f01-8b96-e6013de7078c", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] }, - "protocolMappers" : [ { - "id" : "4cb5d24c-3f20-4e05-b918-feb5b7fbe7bb", - "name" : "auth_time", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usersessionmodel-note-mapper", - "consentRequired" : false, - "config" : { - "user.session.note" : "AUTH_TIME", - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "auth_time", - "jsonType.label" : "long" + { + "id": "611045a9-bade-435d-b549-b62e497d44a5", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "74141972-4429-4565-b576-26ee4b374060", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "039a1f5a-d31b-4e4e-a401-e02694d1cb26", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" } - }, { - "id" : "d067706a-9d23-456b-acb1-253f62f663c5", - "name" : "sub", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-sub-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" + }, + { + "id": "8d580f89-f9cd-4259-ab24-affe163ff5e7", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" } - } ] - } ], - "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr", "basic" ], - "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], - "browserSecurityHeaders" : { - "contentSecurityPolicyReportOnly" : "", - "xContentTypeOptions" : "nosniff", - "referrerPolicy" : "no-referrer", - "xRobotsTag" : "none", - "xFrameOptions" : "SAMEORIGIN", - "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection" : "1; mode=block", - "strictTransportSecurity" : "max-age=31536000; includeSubDomains" - }, - "smtpServer" : { }, - "eventsEnabled" : false, - "eventsListeners" : [ "jboss-logging" ], - "enabledEventTypes" : [ ], - "adminEventsEnabled" : false, - "adminEventsDetailsEnabled" : false, - "identityProviders" : [ ], - "identityProviderMappers" : [ ], - "internationalizationEnabled" : false, - "supportedLocales" : [ ], - "authenticationFlows" : [ { - "id" : "fda8b12c-9bb9-4c04-860a-5b69202d5714", - "alias" : "Account verification options", - "description" : "Method with which to verity the existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-email-verification", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Verify Existing Account by Re-authentication", - "userSetupAllowed" : false - } ] - }, { - "id" : "ff955351-02a7-4978-9dc8-3a7c3e46fb91", - "alias" : "Browser - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "f778c69e-8c53-427c-a299-5d49906aea63", - "alias" : "Direct Grant - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "d3d5c34e-35cd-49aa-8e03-90dce67080ee", - "alias" : "First broker login - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "49d5fa70-ee56-4b5b-8383-05c1a1ca8cc4", - "alias" : "Handle Existing Account", - "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-confirm-link", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Account verification options", - "userSetupAllowed" : false - } ] - }, { - "id" : "9eaf459c-709f-46d7-b312-bb3d01bf561e", - "alias" : "Reset - Conditional OTP", - "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "8a6ccaa6-96a0-45a7-af86-69238b8762ea", - "alias" : "User creation or linking", - "description" : "Flow for the existing/non-existing user alternatives", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "create unique user config", - "authenticator" : "idp-create-user-if-unique", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Handle Existing Account", - "userSetupAllowed" : false - } ] - }, { - "id" : "a726d5c4-022e-475b-bf0d-57e9dcb911fa", - "alias" : "Verify Existing Account by Re-authentication", - "description" : "Reauthentication of existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "First broker login - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "86523e55-21bf-44c9-b107-6f853f0f168a", - "alias" : "browser", - "description" : "browser based authentication", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-cookie", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-spnego", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "identity-provider-redirector", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 25, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "forms", - "userSetupAllowed" : false - } ] - }, { - "id" : "2a4f0f8e-5e32-44b9-b820-4191fb1d4ffc", - "alias" : "clients", - "description" : "Base authentication for clients", - "providerId" : "client-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "client-secret", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-secret-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-x509", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 40, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "afd90c4f-9ed8-4ba5-9cec-1f05cabf610a", - "alias" : "direct grant", - "description" : "OpenID Connect Resource Owner Grant", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "direct-grant-validate-username", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "Direct Grant - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "ceac3589-19df-47f7-ad07-2077ea1fc5d4", - "alias" : "docker auth", - "description" : "Used by Docker clients to authenticate against the IDP", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "docker-http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "ba752550-cfc6-48b3-a521-4972685f42b2", - "alias" : "first broker login", - "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "review profile config", - "authenticator" : "idp-review-profile", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "User creation or linking", - "userSetupAllowed" : false - } ] - }, { - "id" : "ed913470-af37-429c-af89-cc1397fb4147", - "alias" : "forms", - "description" : "Username, password, otp and other auth forms.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Browser - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "295facf5-3aca-4b02-a0cf-360593092a46", - "alias" : "registration", - "description" : "registration flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-page-form", - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : true, - "flowAlias" : "registration form", - "userSetupAllowed" : false - } ] - }, { - "id" : "514b1696-d644-4f01-8b96-e6013de7078c", - "alias" : "registration form", - "description" : "registration form", - "providerId" : "form-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-user-creation", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-password-action", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 50, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-recaptcha-action", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 60, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-terms-and-conditions", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 70, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "611045a9-bade-435d-b549-b62e497d44a5", - "alias" : "reset credentials", - "description" : "Reset credentials for a user if they forgot their password or something", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "reset-credentials-choose-user", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-credential-email", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 40, - "autheticatorFlow" : true, - "flowAlias" : "Reset - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "74141972-4429-4565-b576-26ee4b374060", - "alias" : "saml ecp", - "description" : "SAML ECP Profile Authentication Flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - } ], - "authenticatorConfig" : [ { - "id" : "039a1f5a-d31b-4e4e-a401-e02694d1cb26", - "alias" : "create unique user config", - "config" : { - "require.password.update.after.registration" : "false" } - }, { - "id" : "8d580f89-f9cd-4259-ab24-affe163ff5e7", - "alias" : "review profile config", - "config" : { - "update.profile.on.first.login" : "missing" + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": false, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} } - } ], - "requiredActions" : [ { - "alias" : "CONFIGURE_TOTP", - "name" : "Configure OTP", - "providerId" : "CONFIGURE_TOTP", - "enabled" : true, - "defaultAction" : false, - "priority" : 10, - "config" : { } - }, { - "alias" : "TERMS_AND_CONDITIONS", - "name" : "Terms and Conditions", - "providerId" : "TERMS_AND_CONDITIONS", - "enabled" : false, - "defaultAction" : false, - "priority" : 20, - "config" : { } - }, { - "alias" : "UPDATE_PASSWORD", - "name" : "Update Password", - "providerId" : "UPDATE_PASSWORD", - "enabled" : false, - "defaultAction" : false, - "priority" : 30, - "config" : { } - }, { - "alias" : "UPDATE_PROFILE", - "name" : "Update Profile", - "providerId" : "UPDATE_PROFILE", - "enabled" : true, - "defaultAction" : false, - "priority" : 40, - "config" : { } - }, { - "alias" : "VERIFY_EMAIL", - "name" : "Verify Email", - "providerId" : "VERIFY_EMAIL", - "enabled" : true, - "defaultAction" : false, - "priority" : 50, - "config" : { } - }, { - "alias" : "delete_account", - "name" : "Delete Account", - "providerId" : "delete_account", - "enabled" : false, - "defaultAction" : false, - "priority" : 60, - "config" : { } - }, { - "alias" : "webauthn-register", - "name" : "Webauthn Register", - "providerId" : "webauthn-register", - "enabled" : true, - "defaultAction" : false, - "priority" : 70, - "config" : { } - }, { - "alias" : "webauthn-register-passwordless", - "name" : "Webauthn Register Passwordless", - "providerId" : "webauthn-register-passwordless", - "enabled" : true, - "defaultAction" : false, - "priority" : 80, - "config" : { } - }, { - "alias" : "VERIFY_PROFILE", - "name" : "Verify Profile", - "providerId" : "VERIFY_PROFILE", - "enabled" : true, - "defaultAction" : false, - "priority" : 90, - "config" : { } - }, { - "alias" : "delete_credential", - "name" : "Delete Credential", - "providerId" : "delete_credential", - "enabled" : true, - "defaultAction" : false, - "priority" : 100, - "config" : { } - }, { - "alias" : "update_user_locale", - "name" : "Update User Locale", - "providerId" : "update_user_locale", - "enabled" : true, - "defaultAction" : false, - "priority" : 1000, - "config" : { } - } ], - "browserFlow" : "browser", - "registrationFlow" : "registration", - "directGrantFlow" : "direct grant", - "resetCredentialsFlow" : "reset credentials", - "clientAuthenticationFlow" : "clients", - "dockerAuthenticationFlow" : "docker auth", - "firstBrokerLoginFlow" : "first broker login", - "attributes" : { - "cibaBackchannelTokenDeliveryMode" : "poll", - "cibaAuthRequestedUserHint" : "login_hint", - "oauth2DevicePollingInterval" : "5", - "clientOfflineSessionMaxLifespan" : "0", - "clientSessionIdleTimeout" : "0", - "clientOfflineSessionIdleTimeout" : "0", - "cibaInterval" : "5", - "realmReusableOtpCode" : "false", - "cibaExpiresIn" : "120", - "oauth2DeviceCodeLifespan" : "600", - "parRequestUriLifespan" : "60", - "clientSessionMaxLifespan" : "0", - "organizationsEnabled" : "false" + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DevicePollingInterval": "5", + "clientOfflineSessionMaxLifespan": "0", + "clientSessionIdleTimeout": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false", + "cibaExpiresIn": "120", + "oauth2DeviceCodeLifespan": "600", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "organizationsEnabled": "false" }, - "keycloakVersion" : "25.0.6", - "userManagedAccessAllowed" : false, - "organizationsEnabled" : false, - "clientProfiles" : { - "profiles" : [ ] + "keycloakVersion": "25.0.6", + "userManagedAccessAllowed": false, + "organizationsEnabled": false, + "clientProfiles": { + "profiles": [] }, - "clientPolicies" : { - "policies" : [ ] + "clientPolicies": { + "policies": [] } } \ No newline at end of file diff --git a/notification-service/build.gradle b/notification-service/build.gradle index 7f6e30f..9659db7 100644 --- a/notification-service/build.gradle +++ b/notification-service/build.gradle @@ -28,34 +28,44 @@ dependencies { // 1. Web / External API implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' - // 2. Persistence (DB) + // 2. Service Discovery + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + + // 3. Persistence (DB) implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'org.postgresql:postgresql' - // 3. External Services + // 4. External Services implementation 'com.slack.api:slack-api-client:1.45.3' - // 4. QueryDSL + // 5. QueryDSL implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta' annotationProcessor 'jakarta.annotation:jakarta.annotation-api' annotationProcessor 'jakarta.persistence:jakarta.persistence-api' - // 5. Validation + // 6. Validation implementation 'org.springframework.boot:spring-boot-starter-validation' - // 6. Common Module + // 7. Common Module implementation project(':common') implementation 'org.springframework.boot:spring-boot-starter-amqp' - // 7. Lombok + // 8. Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' - // 8. Test + // 9. Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // 10. Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.4' + + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/NotificationserviceApplication.java b/notification-service/src/main/java/com/shipflow/notificationservice/NotificationserviceApplication.java index 64e0338..1b9e7dc 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/NotificationserviceApplication.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/NotificationserviceApplication.java @@ -3,9 +3,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.cloud.openfeign.EnableFeignClients; @ConfigurationPropertiesScan @SpringBootApplication +@EnableFeignClients public class NotificationserviceApplication { public static void main(String[] args) { diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java new file mode 100644 index 0000000..5c51d26 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/NotificationOrchestratorService.java @@ -0,0 +1,217 @@ +package com.shipflow.notificationservice.application; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.shipflow.common.exception.BusinessException; +import com.shipflow.notificationservice.application.ai.AiAppService; +import com.shipflow.notificationservice.application.ai.dto.command.GenerateDeadlineCommand; +import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; +import com.shipflow.notificationservice.application.slack.SlackAppService; +import com.shipflow.notificationservice.application.slack.dto.command.SendSlackMessageCommand; +import com.shipflow.notificationservice.domain.ai.AiLog; +import com.shipflow.notificationservice.domain.ai.exception.AiErrorCode; +import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; +import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; +import com.shipflow.notificationservice.infrastructure.client.order.OrderInternalClient; +import com.shipflow.notificationservice.infrastructure.client.order.OrderReadModelResponse; +import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class NotificationOrchestratorService { + + private static final String DEFAULT_WORKING_HOURS = "09:00 ~ 18:00"; + private static final String UNKNOWN = "확인 필요"; + private static final UUID SYSTEM_USER_ID = new UUID(0L, 0L); + + private final AiAppService aiAppService; + private final SlackAppService slackAppService; + private final AiLogRepository aiLogRepository; + private final OrderInternalClient orderInternalClient; + + public void handleShipmentCreated(ShipmentCreatedEvent event) { + //1. 주문 정보 조회 + OrderReadModelResponse order = getOrderReadModel(event.getOrderId()); + String slackId = resolveSlackId(event); + + // 2. AI 호출 + AiLog 저장 → AiAppService + GenerateDeadlineCommand command = toGenerateDeadlineCommand(event, order, slackId); + AiLogResult aiResult = aiAppService.generateAiLog(command); + // 3. 슬랙 메시지 생성 + String slackMessage = createDeadlineSlackMessage(command, aiResult, order); + // 4. 슬랙 발송 + SlackMessage 저장 → SlackAppService 내부 + try { + slackAppService.sendSlackMessage( + new SendSlackMessageCommand( + SYSTEM_USER_ID, + "MASTER", + slackId, + event.getShipmentId(), + aiResult.aiId(), + slackMessage, + SlackMessageType.DEADLINE_ALERT + ) + ); + // 5. 슬랙 발송 성공 → AiLog 상태 업데이트 (별도 트랜잭션) + markSlackSendSuccess(aiResult.aiId()); + } catch (Exception e) { + // 6. 슬랙 발송 실패 → AiLog 상태 업데이트 (별도 트랜잭션) + markSlackSendFail(aiResult.aiId()); + throw e; + } + } + + private GenerateDeadlineCommand toGenerateDeadlineCommand( + ShipmentCreatedEvent event, + OrderReadModelResponse order, + String slackId + ) { + return new GenerateDeadlineCommand( + event.getOrderId(), + SYSTEM_USER_ID, + event.getShipmentId(), + null, + slackId, + + null, + null, + + event.getProductId(), + extractProductText(order), + event.getQuantity(), + + event.getDepartureHubId(), + order != null ? order.departureHubName() : UNKNOWN, + event.getArrivalHubId(), + order != null ? order.arrivalHubName() : UNKNOWN, + extractRouteTexts(event), + + extractRequestNote(event), + event.getRequestDeadline(), + DEFAULT_WORKING_HOURS, + + AiRequestType.DEADLINE, + null + ); + } + + private OrderReadModelResponse getOrderReadModel(UUID orderId) { + if (orderId == null) + return null; + try { + return orderInternalClient.getOrderReadModel(orderId); + } catch (Exception e) { + return null; + } + } + + private String resolveSlackId(ShipmentCreatedEvent event) { + String slackId = event.getShipmentManagerSlackId(); + return (slackId == null || slackId.isBlank()) ? UNKNOWN : slackId; + } + + private String extractProductText(OrderReadModelResponse order) { + return (order == null || order.productName() == null || order.productName().isBlank()) + ? UNKNOWN + : order.productName(); + } + + private List extractRouteTexts(ShipmentCreatedEvent event) { + if (event.getRoutes() == null || event.getRoutes().isEmpty()) { + return Collections.emptyList(); + } + + return event.getRoutes().stream() + .map(route -> { + if (route.getSequence() == null) { + return "경유"; + } + return route.getSequence() + "번 경유"; + }) + .toList(); + } + + private String extractRequestNote(ShipmentCreatedEvent event) { + return (event.getRequestNote() == null || event.getRequestNote().isBlank()) + ? "없음" + : event.getRequestNote(); + } + + private String createDeadlineSlackMessage( + GenerateDeadlineCommand command, + AiLogResult aiResult, + OrderReadModelResponse order + ) { + String routeText = (command.route() == null || command.route().isEmpty()) + ? "없음" + : String.join("\n", command.route()); + + String requestNote = (command.requestNote() == null || command.requestNote().isBlank()) + ? "없음" + : command.requestNote(); + + String ordererName = (order == null || order.ordererName() == null) + ? UNKNOWN + : order.ordererName(); + + String orderTime = (order == null || order.createdAt() == null) + ? UNKNOWN + : order.createdAt().toString(); + + return """ + 🚚 배송 요청 알림 + + 주문 번호: %s + 주문자 정보: %s + 주문 시간: %s + 배송 번호: %s + 상품 정보: %s / 수량: %d개 + 요청 사항: %s + 납기 기한: %s + + 발송지: %s + 경유 경로: + %s + 도착지: %s + + ⏰ AI 계산 최종 발송 시한: %s + + ※ 해당 시간 이전에 발송을 완료해주세요. + """.formatted( + command.orderId(), + ordererName, + orderTime, + command.relatedShipmentId(), + command.product(), + command.quantity(), // 수량 + requestNote, + command.deadline(), // 납기 기한 + command.fromHub(), + routeText, + command.toHub(), + aiResult.finalDeadlineAt() + ); + } + + @Transactional + public void markSlackSendSuccess(UUID aiLogId) { + AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) + .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); + aiLog.markSendSuccess(); + } + + @Transactional + public void markSlackSendFail(UUID aiLogId) { + AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiLogId) + .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); + aiLog.markSendFail(); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java index 882e990..e91301e 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/AiAppService.java @@ -9,6 +9,7 @@ import com.shipflow.common.exception.BusinessException; import com.shipflow.notificationservice.application.ai.dto.command.GenerateDeadlineCommand; +import com.shipflow.notificationservice.application.ai.dto.command.SearchAiLogCommand; import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; import com.shipflow.notificationservice.domain.ai.AiGenerator; import com.shipflow.notificationservice.domain.ai.AiLog; @@ -35,15 +36,15 @@ public AiLogResult generateAiLog(GenerateDeadlineCommand command) { String prompt = createDeadlinePrompt(command); - AiLog aiLog = aiLogRepository.save( - new AiLog( - command.relatedShipmentId(), - command.shipmentManagerId(), - prompt, - command.requestType() - ) + AiLog aiLog = new AiLog( + command.relatedShipmentId(), + command.shipmentManagerId(), + prompt, + command.requestType() ); + aiLog = aiLogRepository.save(aiLog); + try { AiResponseInfo result = aiGenerator.generate(prompt); @@ -63,15 +64,22 @@ public AiLogResult generateAiLog(GenerateDeadlineCommand command) { } } - public AiLogResult getAiLog(UUID aiId) { + public AiLogResult getAiLog(UUID userId, String userRole, UUID aiId) { + validateMasterRole(userRole); + AiLog aiLog = aiLogRepository.findByIdAndDeletedAtIsNull(aiId) .orElseThrow(() -> new BusinessException(AiErrorCode.AI_LOG_NOT_FOUND)); return AiLogResult.from(aiLog); } - public Page getAiLogs(Pageable pageable) { - return aiLogRepository.findAllByDeletedAtIsNull(pageable) + public Page getAiLogs( + SearchAiLogCommand command, + Pageable pageable + ) { + validateMasterRole(command.userRole()); + + return aiLogRepository.search(command, pageable) .map(AiLogResult::from); } @@ -79,6 +87,12 @@ private void validateCommand(GenerateDeadlineCommand command) { if (command == null) { throw new BusinessException(AiErrorCode.AI_EVENT_NOT_FOUND); } + if (command.orderId() == null) { + throw new BusinessException(AiErrorCode.AI_EVENT_INVALID); + } + if (command.ordererId() == null) { + throw new BusinessException(AiErrorCode.AI_EVENT_INVALID); + } if (command.requestType() == null) { throw new BusinessException(AiErrorCode.AI_REQUEST_TYPE_REQUIRED); } @@ -94,6 +108,9 @@ private void validateCommand(GenerateDeadlineCommand command) { if (command.product() == null || command.product().isBlank()) { throw new BusinessException(AiErrorCode.AI_PRODUCT_REQUIRED); } + if (command.quantity() == null || command.quantity() <= 0) { + throw new BusinessException(AiErrorCode.AI_EVENT_INVALID); + } if (command.deadline() == null) { throw new BusinessException(AiErrorCode.AI_DEADLINE_REQUIRED); } @@ -115,10 +132,12 @@ private String createDeadlinePrompt(GenerateDeadlineCommand command) { return """ 다음 물류 정보를 바탕으로 최종 발송 시한을 계산해라. + 주문 번호: %s 발송지: %s 경유지: %s 도착지: %s 상품: %s + 수량: %d 요청사항: %s 납기: %s 근무시간: %s @@ -126,13 +145,21 @@ private String createDeadlinePrompt(GenerateDeadlineCommand command) { 반드시 ISO-8601 형식의 발송 시한만 포함해서 응답해라. 예시: 2026-04-04T09:00:00 """.formatted( + command.orderId(), command.fromHub(), routeText, command.toHub(), command.product(), + command.quantity(), requestNote, command.deadline(), workingHours ); } + + private void validateMasterRole(String userRole) { + if (!"MASTER".equals(userRole)) { + throw new BusinessException(AiErrorCode.FORBIDDEN_AI_ACCESS); + } + } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java index 2128838..165ee6a 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/GenerateDeadlineCommand.java @@ -8,16 +8,30 @@ import com.shipflow.notificationservice.domain.ai.type.AiRequestType; public record GenerateDeadlineCommand( + UUID orderId, + UUID ordererId, UUID relatedShipmentId, UUID shipmentManagerId, + String receiverSlackId, + + UUID supplierCompanyId, + UUID receiverCompanyId, + + UUID productId, + String product, + Integer quantity, + + UUID departureHubId, String fromHub, + UUID arrivalHubId, String toHub, List route, - String product, + String requestNote, LocalDateTime deadline, String workingHours, + AiRequestType requestType, LocalDate workDate ) { -} +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/SearchAiLogCommand.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/SearchAiLogCommand.java new file mode 100644 index 0000000..8fe501d --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/ai/dto/command/SearchAiLogCommand.java @@ -0,0 +1,20 @@ +package com.shipflow.notificationservice.application.ai.dto.command; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.notificationservice.domain.ai.type.AiRequestStatus; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; + +public record SearchAiLogCommand( + UUID userId, + String userRole, + UUID shipmentManagerId, + AiRequestType requestType, + AiRequestStatus requestStatus, + LocalDate workDate, + LocalDateTime createdAtFrom, + LocalDateTime createdAtTo +) { +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java index 8abf7ce..99972ee 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/SlackAppService.java @@ -1,12 +1,15 @@ package com.shipflow.notificationservice.application.slack; -import java.util.List; +import java.util.Set; import java.util.UUID; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.shipflow.common.exception.BusinessException; +import com.shipflow.notificationservice.application.slack.dto.command.SearchSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.command.SendSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.command.UpdateSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.result.SlackMessageResult; @@ -27,20 +30,21 @@ public class SlackAppService { private final SlackSender slackSender; // 메시지 전송 - // TODO: 인증 적용 후 userId 받아 createdBy 처리 @Transactional public SlackMessageResult sendSlackMessage(SendSlackMessageCommand command) { - - SlackMessage slackMessage = slackMessageRepository.save( - new SlackMessage( - command.receiverSlackId(), - command.relatedShipmentId(), - command.relatedAiLogId(), - command.message(), - command.messageType() - ) + //권한 확인 + validateCreateRole(command.userRole()); + + SlackMessage slackMessage = new SlackMessage( + command.receiverSlackId(), + command.relatedShipmentId(), + command.relatedAiLogId(), + command.message(), + command.messageType() ); + slackMessage = slackMessageRepository.save(slackMessage); + try { SlackSendInfo result = slackSender.sendMessage( command.receiverSlackId(), @@ -55,7 +59,9 @@ public SlackMessageResult sendSlackMessage(SendSlackMessageCommand command) { } // 단건 조회 - public SlackMessageResult getSlackMessage(UUID slackId) { + public SlackMessageResult getSlackMessage(UUID userId, String userRole, UUID slackId) { + //권한 확인 + validateMasterRole(userRole); SlackMessage slackMessage = slackMessageRepository.findByIdAndDeletedAtIsNull(slackId) .orElseThrow(() -> new BusinessException(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND)); @@ -63,17 +69,22 @@ public SlackMessageResult getSlackMessage(UUID slackId) { } // 목록 조회 - public List getSlackMessages() { - return slackMessageRepository.findAllByDeletedAtIsNull() - .stream() - .map(SlackMessageResult::from) - .toList(); + public Page getSlackMessages( + SearchSlackMessageCommand command, + Pageable pageable + ) { + //권한 확인 + validateMasterRole(command.userRole()); + + return slackMessageRepository.search(command, pageable) + .map(SlackMessageResult::from); } //슬랙 메세지 수정 - // TODO: 인증 적용 후 userId 받아 updatedBy 처리 @Transactional public SlackMessageResult updateSlackMessage(UpdateSlackMessageCommand command) { + //권한 확인 + validateMasterRole(command.userRole()); SlackMessage slackMessage = slackMessageRepository.findByIdAndDeletedAtIsNull(command.slackId()) .orElseThrow(() -> new BusinessException(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND)); @@ -92,13 +103,14 @@ public SlackMessageResult updateSlackMessage(UpdateSlackMessageCommand command) } //슬랙 메세지 삭제 - // TODO: 인증 적용 후 userId 받아 deletedBy 처리 @Transactional - public void deleteSlackMessage(UUID slackId, UUID userId) { + public void deleteSlackMessage(UUID userId, String userRole, UUID slackId) { + //권한 확인 + validateMasterRole(userRole); + SlackMessage slackMessage = slackMessageRepository.findByIdAndDeletedAtIsNull(slackId) .orElseThrow(() -> new BusinessException(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND)); - // 도메인 검증을 먼저 수행한 후, 외부 Slack 삭제 API 호출 slackMessage.validateDeletable(); slackSender.deleteMessage( @@ -108,4 +120,16 @@ public void deleteSlackMessage(UUID slackId, UUID userId) { slackMessage.markDeleted(userId); } + + private void validateCreateRole(String userRole) { + if (!Set.of("MASTER", "HUB_MANAGER", "SHIPMENT_MANAGER", "COMPANY_MANAGER").contains(userRole)) { + throw new BusinessException(SlackErrorCode.FORBIDDEN_SLACK_ACCESS); + } + } + + private void validateMasterRole(String userRole) { + if (!"MASTER".equals(userRole)) { + throw new BusinessException(SlackErrorCode.FORBIDDEN_SLACK_ACCESS); + } + } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java new file mode 100644 index 0000000..42d9260 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SearchSlackMessageCommand.java @@ -0,0 +1,18 @@ +package com.shipflow.notificationservice.application.slack.dto.command; + +import java.time.LocalDateTime; +import java.util.UUID; + +import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; +import com.shipflow.notificationservice.domain.slack.type.SlackSendStatus; + +public record SearchSlackMessageCommand( + UUID userId, + String userRole, + String receiverSlackId, + SlackSendStatus sendStatus, + SlackMessageType messageType, + LocalDateTime createdAtFrom, + LocalDateTime createdAtTo +) { +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SendSlackMessageCommand.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SendSlackMessageCommand.java index c349b9c..05259d5 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SendSlackMessageCommand.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/SendSlackMessageCommand.java @@ -5,6 +5,8 @@ import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; public record SendSlackMessageCommand( + UUID userId, + String userRole, String receiverSlackId, UUID relatedShipmentId, UUID relatedAiLogId, diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/UpdateSlackMessageCommand.java b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/UpdateSlackMessageCommand.java index 05aaa8a..6dda7fd 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/UpdateSlackMessageCommand.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/application/slack/dto/command/UpdateSlackMessageCommand.java @@ -3,6 +3,8 @@ import java.util.UUID; public record UpdateSlackMessageCommand( + UUID userId, + String userRole, UUID slackId, String message ) { diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java index eb51ec0..2173159 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/config/JPAConfig.java @@ -11,6 +11,8 @@ import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -24,6 +26,8 @@ @EntityScan(basePackages = "com.shipflow.notificationservice") public class JPAConfig { + private static final UUID SYSTEM_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); + @PersistenceContext private EntityManager em; @@ -36,11 +40,22 @@ public JPAQueryFactory jpaQueryFactory() { @Bean public AuditorAware auditorAware() { return () -> { - /* - * TODO: Spring Security 연동 시 아래 방식으로 변경 - * - SecurityContext에서 로그인 사용자 UUID 추출하여 반환 - */ - return Optional.of(UUID.fromString("00000000-0000-0000-0000-000000000001")); + try { + ServletRequestAttributes attrs = + (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); + + // 요청 컨텍스트 없는 경우만 SYSTEM_UUID (RabbitMQ 등) + if (attrs == null) + return Optional.of(SYSTEM_UUID); + + String userId = attrs.getRequest().getHeader("X-User-Id"); + if (userId == null || userId.isBlank()) + throw new IllegalStateException("X-User-Id 헤더가 없습니다."); + + return Optional.of(UUID.fromString(userId)); + } catch (Exception e) { + return Optional.of(SYSTEM_UUID); + } }; } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/config/RedisConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/config/RedisConfig.java new file mode 100644 index 0000000..4a64371 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/config/RedisConfig.java @@ -0,0 +1,20 @@ +package com.shipflow.notificationservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + return template; + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/config/SwaggerConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/config/SwaggerConfig.java new file mode 100644 index 0000000..7994772 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/config/SwaggerConfig.java @@ -0,0 +1,20 @@ +package com.shipflow.notificationservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("Notification Service API") + .description("슬랙 메시지 및 AI 발송 시한 관리 API") + .version("v1")); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java index f0b5df8..1214319 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/AiLog.java @@ -45,7 +45,7 @@ public class AiLog extends BaseEntity { @Column(name = "work_date") private LocalDate workDate; - + @Enumerated(EnumType.STRING) @Column(name = "send_status", length = 20, nullable = false) private SlackSendStatus sendStatus; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java index 92ceb34..4ca196a 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/exception/AiErrorCode.java @@ -7,6 +7,7 @@ public enum AiErrorCode implements ErrorCode { // 조회 + FORBIDDEN_AI_ACCESS("FORBIDDEN_AI_ACCESS", HttpStatus.FORBIDDEN, "AI 접근 권한이 없습니다."), AI_LOG_NOT_FOUND("AI_LOG_NOT_FOUND", HttpStatus.NOT_FOUND, "AI 로그를 찾을 수 없습니다."), // 이벤트 diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java index a33f564..a380bee 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/ai/repository/AiLogRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import com.shipflow.notificationservice.application.ai.dto.command.SearchAiLogCommand; import com.shipflow.notificationservice.domain.ai.AiLog; public interface AiLogRepository { @@ -14,5 +15,5 @@ public interface AiLogRepository { Optional findByIdAndDeletedAtIsNull(UUID id); - Page findAllByDeletedAtIsNull(Pageable pageable); + Page search(SearchAiLogCommand command, Pageable pageable); } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java index 64e99d3..7ea452b 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/SlackMessage.java @@ -82,6 +82,13 @@ public void markSuccess(String slackTs, String slackChannelId) { this.sentAt = LocalDateTime.now(); } + public void updateMessage(String newMessage) { // userId 파라미터 제거 + if (newMessage == null || newMessage.isBlank()) { + throw new BusinessException(SlackErrorCode.SLACK_MESSAGE_REQUIRED); + } + this.message = newMessage; + } + public void markDeleted(UUID userId) { validateDeletable(); super.softDelete(userId); @@ -105,14 +112,6 @@ public void validateUpdatable() { } } - public void updateMessage(String newMessage) { - if (newMessage == null || newMessage.isBlank()) { - throw new BusinessException(SlackErrorCode.SLACK_MESSAGE_REQUIRED); - } - - this.message = newMessage; - } - public void validateDeletable() { if (this.getDeletedAt() != null) { throw new BusinessException(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND); @@ -126,5 +125,5 @@ public void validateDeletable() { public void markFail() { this.sendStatus = SlackSendStatus.FAIL; } - + } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/exception/SlackErrorCode.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/exception/SlackErrorCode.java index 713116a..75cdd60 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/exception/SlackErrorCode.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/exception/SlackErrorCode.java @@ -6,6 +6,7 @@ public enum SlackErrorCode implements ErrorCode { //슬랙 발송 + FORBIDDEN_SLACK_ACCESS("FORBIDDEN_SLACK_ACCESS", HttpStatus.FORBIDDEN, "슬랙 메시지 접근 권한이 없습니다."), SLACK_MESSAGE_NOT_FOUND("SLACK_MESSAGE_NOT_FOUND", HttpStatus.NOT_FOUND, "슬랙 메시지를 찾을 수 없습니다."), SLACK_SEND_FAILED("SLACK_SEND_FAILED", HttpStatus.BAD_REQUEST, "슬랙 메시지 발송에 실패했습니다."), INVALID_SLACK_ID_FORMAT("INVALID_SLACK_ID_FORMAT", HttpStatus.BAD_REQUEST, "지원하지 않는 Slack ID 형식입니다."), diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java index ebe19f2..42b22af 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/domain/slack/repository/SlackMessageRepository.java @@ -1,9 +1,12 @@ package com.shipflow.notificationservice.domain.slack.repository; -import java.util.List; import java.util.Optional; import java.util.UUID; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.shipflow.notificationservice.application.slack.dto.command.SearchSlackMessageCommand; import com.shipflow.notificationservice.domain.slack.SlackMessage; public interface SlackMessageRepository { @@ -12,5 +15,5 @@ public interface SlackMessageRepository { Optional findByIdAndDeletedAtIsNull(UUID slackId); - List findAllByDeletedAtIsNull(); + Page search(SearchSlackMessageCommand command, Pageable pageable); } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java index e06fa99..4f2f801 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/GeminiApiClient.java @@ -6,6 +6,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; @@ -17,11 +18,9 @@ import com.shipflow.notificationservice.infrastructure.client.ai.dto.GeminiRequest; import com.shipflow.notificationservice.infrastructure.client.ai.dto.GeminiResponse; -import lombok.RequiredArgsConstructor; import reactor.core.publisher.Mono; @Component -@RequiredArgsConstructor public class GeminiApiClient implements AiGenerator { private static final String API_KEY_HEADER = "x-goog-api-key"; @@ -32,9 +31,17 @@ public class GeminiApiClient implements AiGenerator { private final WebClient webClient; private final GeminiProperties geminiProperties; + public GeminiApiClient( + @Qualifier("geminiWebClient") WebClient webClient, + GeminiProperties geminiProperties + ) { + this.webClient = webClient; + this.geminiProperties = geminiProperties; + } + @Override public AiResponseInfo generate(String prompt) { - + validatePrompt(prompt); GeminiResponse response; diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java index 18f2cab..a208d05 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/ai/config/GeminiApiConfig.java @@ -1,15 +1,28 @@ package com.shipflow.notificationservice.infrastructure.client.ai.config; +import java.time.Duration; + import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; +import io.netty.channel.ChannelOption; +import reactor.netty.http.client.HttpClient; + @Configuration @EnableConfigurationProperties(GeminiProperties.class) public class GeminiApiConfig { - @Bean - public WebClient webClient() { - return WebClient.builder().build(); + + @Bean(name = "geminiWebClient") + public WebClient geminiWebClient() { + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .responseTimeout(Duration.ofSeconds(30)); + + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); } -} +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java new file mode 100644 index 0000000..faea649 --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderInternalClient.java @@ -0,0 +1,14 @@ +package com.shipflow.notificationservice.infrastructure.client.order; + +import java.util.UUID; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient(name = "orderservice") +public interface OrderInternalClient { + + @GetMapping("/internal/orders/{orderId}/read-model") + OrderReadModelResponse getOrderReadModel(@PathVariable UUID orderId); +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java new file mode 100644 index 0000000..6130e7a --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/client/order/OrderReadModelResponse.java @@ -0,0 +1,18 @@ +package com.shipflow.notificationservice.infrastructure.client.order; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record OrderReadModelResponse( + UUID orderId, + UUID productId, + String productName, + int quantity, + String ordererName, + LocalDateTime createdAt, + String departureHubName, + String arrivalHubName, + LocalDateTime requestDeadline, + String requestNote +) { +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java deleted file mode 100644 index 6b7468e..0000000 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedConsumer.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.shipflow.notificationservice.infrastructure.messaging.consumer; - -import org.springframework.amqp.rabbit.annotation.RabbitListener; -import org.springframework.stereotype.Component; - -import com.shipflow.notificationservice.infrastructure.messaging.config.NotificationRabbitConfig; -import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Component -public class ShipmentCreatedConsumer { - - @RabbitListener(queues = NotificationRabbitConfig.QUEUE_NOTIFICATION_SHIPMENT_CREATED) - public void handleShipmentCreated(ShipmentCreatedEvent event) { - log.info( - "[ShipmentCreatedConsumer] shipment.created 수신 - orderId={}, ordererId={}, supplierCompanyId={}, receiverCompanyId={}, productId={}, quantity={}, departureHubId={}, arrivalHubId={}, requestDeadline={}, requestNote={}, occurredAt={}", - event.getOrderId(), - event.getOrdererId(), - event.getSupplierCompanyId(), - event.getReceiverCompanyId(), - event.getProductId(), - event.getQuantity(), - event.getDepartureHubId(), - event.getArrivalHubId(), - event.getRequestDeadline(), - event.getRequestNote(), - event.getOccurredAt() - ); - } -} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java new file mode 100644 index 0000000..ebbd83b --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedHandler.java @@ -0,0 +1,56 @@ +package com.shipflow.notificationservice.infrastructure.messaging.consumer; + +import java.time.Duration; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import com.shipflow.common.messaging.handler.AbstractSagaHandler; +import com.shipflow.notificationservice.application.NotificationOrchestratorService; +import com.shipflow.notificationservice.infrastructure.messaging.config.NotificationRabbitConfig; +import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class ShipmentCreatedHandler extends AbstractSagaHandler { + + private final NotificationOrchestratorService notificationOrchestratorService; + private final RedisTemplate redisTemplate; + private static final Duration IDEMPOTENCY_TTL = Duration.ofHours(24); + + public ShipmentCreatedHandler( + NotificationOrchestratorService notificationOrchestratorService, + RedisTemplate redisTemplate + ) { + this.notificationOrchestratorService = notificationOrchestratorService; + this.redisTemplate = redisTemplate; + } + + @RabbitListener(queues = NotificationRabbitConfig.QUEUE_NOTIFICATION_SHIPMENT_CREATED) + public void receive(ShipmentCreatedEvent event) { + handle(event); + } + + @Override + protected void process(ShipmentCreatedEvent event) { + String key = "saga:processed:" + event.getEventId(); + + Boolean isNew = redisTemplate.opsForValue() + .setIfAbsent(key, "1", IDEMPOTENCY_TTL); + + if (Boolean.FALSE.equals(isNew)) { + log.warn("중복 이벤트 무시 eventId={}", event.getEventId()); + return; + } + + try { + notificationOrchestratorService.handleShipmentCreated(event); + } catch (Exception e) { + redisTemplate.delete(key); + throw e; + } + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedListener.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedListener.java new file mode 100644 index 0000000..ce0761e --- /dev/null +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/consumer/ShipmentCreatedListener.java @@ -0,0 +1,21 @@ +package com.shipflow.notificationservice.infrastructure.messaging.consumer; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import com.shipflow.notificationservice.infrastructure.messaging.config.NotificationRabbitConfig; +import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ShipmentCreatedListener { + + private final ShipmentCreatedHandler shipmentCreatedHandler; + + @RabbitListener(queues = NotificationRabbitConfig.QUEUE_NOTIFICATION_SHIPMENT_CREATED) + public void onShipmentCreated(ShipmentCreatedEvent event) { + shipmentCreatedHandler.handle(event); + } +} \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java index 6c529b6..e55b85f 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/messaging/dto/ShipmentCreatedEvent.java @@ -1,25 +1,48 @@ package com.shipflow.notificationservice.infrastructure.messaging.dto; import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.shipflow.common.messaging.event.SagaEvent; + +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor -public class ShipmentCreatedEvent { +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class ShipmentCreatedEvent extends SagaEvent { - private UUID shipmentId; private UUID orderId; - private UUID ordererId; - private UUID supplierCompanyId; - private UUID receiverCompanyId; + private UUID shipmentId; + private UUID productId; private Integer quantity; + private UUID departureHubId; private UUID arrivalHubId; + private LocalDateTime requestDeadline; private String requestNote; - private LocalDateTime occurredAt; + + //Todo: 생성 확인 + private String shipmentManagerSlackId; + + private List routes; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + @JsonIgnoreProperties(ignoreUnknown = true) + public static class RouteInfo { + private Integer sequence; + private UUID departureHubId; + private UUID arrivalHubId; + } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java index fbd1260..fcfc365 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogJpaRepository.java @@ -3,8 +3,6 @@ import java.util.Optional; import java.util.UUID; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import com.shipflow.notificationservice.domain.ai.AiLog; @@ -12,7 +10,4 @@ public interface AiLogJpaRepository extends JpaRepository { Optional findByIdAndDeletedAtIsNull(UUID id); - - Page findAllByDeletedAtIsNull(Pageable pageable); - } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java index 57895b8..b8f7931 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/ai/AiLogRepositoryImpl.java @@ -1,14 +1,25 @@ package com.shipflow.notificationservice.infrastructure.persistence.ai; +import static com.shipflow.notificationservice.domain.ai.QAiLog.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import java.util.UUID; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.shipflow.notificationservice.application.ai.dto.command.SearchAiLogCommand; import com.shipflow.notificationservice.domain.ai.AiLog; import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; +import com.shipflow.notificationservice.domain.ai.type.AiRequestStatus; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; import lombok.RequiredArgsConstructor; @@ -17,6 +28,7 @@ public class AiLogRepositoryImpl implements AiLogRepository { private final AiLogJpaRepository aiLogJpaRepository; + private final JPAQueryFactory queryFactory; @Override public AiLog save(AiLog aiLog) { @@ -29,8 +41,56 @@ public Optional findByIdAndDeletedAtIsNull(UUID aiId) { } @Override - public Page findAllByDeletedAtIsNull(Pageable pageable) { - return aiLogJpaRepository.findAllByDeletedAtIsNull(pageable); + public Page search(SearchAiLogCommand command, Pageable pageable) { + BooleanBuilder builder = new BooleanBuilder(); + builder.and(aiLog.deletedAt.isNull()); + builder.and(shipmentManagerIdEq(command.shipmentManagerId())); + builder.and(requestTypeEq(command.requestType())); + builder.and(requestStatusEq(command.requestStatus())); + builder.and(workDateEq(command.workDate())); + builder.and(createdAtGoe(command.createdAtFrom())); + builder.and(createdAtLoe(command.createdAtTo())); + + List content = queryFactory + .selectFrom(aiLog) + .where(builder) + .orderBy(aiLog.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(aiLog.id.count()) + .from(aiLog) + .where(builder) + .fetchOne(); + + return new PageImpl<>(content, pageable, total == null ? 0L : total); } + + private com.querydsl.core.types.Predicate shipmentManagerIdEq(UUID shipmentManagerId) { + return shipmentManagerId == null ? null : aiLog.shipmentManagerId.eq(shipmentManagerId); + } + + private com.querydsl.core.types.Predicate requestTypeEq(AiRequestType requestType) { + return requestType == null ? null : aiLog.requestType.eq(requestType); + } + + private com.querydsl.core.types.Predicate requestStatusEq(AiRequestStatus requestStatus) { + return requestStatus == null ? null : aiLog.requestStatus.eq(requestStatus); + } + + private com.querydsl.core.types.Predicate workDateEq(LocalDate workDate) { + return workDate == null ? null : aiLog.workDate.eq(workDate); + } + + private com.querydsl.core.types.Predicate createdAtGoe(LocalDateTime from) { + return from == null ? null : aiLog.createdAt.goe(from); + } + + private com.querydsl.core.types.Predicate createdAtLoe(LocalDateTime to) { + return to == null ? null : aiLog.createdAt.loe(to); + } + } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java index 9d1a8b1..009d6e6 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageJpaRepository.java @@ -1,6 +1,5 @@ package com.shipflow.notificationservice.infrastructure.persistence.slack; -import java.util.List; import java.util.Optional; import java.util.UUID; @@ -10,10 +9,6 @@ public interface SlackMessageJpaRepository extends JpaRepository { - // TODO: 단건 조회 시 soft delete 제외 Optional findByIdAndDeletedAtIsNull(UUID id); - // TODO: 목록 조회 페이징 및 검색 처리 필요 - List findAllByDeletedAtIsNull(); - -} \ No newline at end of file +} diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java index 14a7307..facd7f9 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/infrastructure/persistence/slack/SlackMessageRepositoryImpl.java @@ -1,13 +1,24 @@ package com.shipflow.notificationservice.infrastructure.persistence.slack; +import static com.shipflow.notificationservice.domain.slack.QSlackMessage.*; + +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.UUID; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.shipflow.notificationservice.application.slack.dto.command.SearchSlackMessageCommand; import com.shipflow.notificationservice.domain.slack.SlackMessage; import com.shipflow.notificationservice.domain.slack.repository.SlackMessageRepository; +import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; +import com.shipflow.notificationservice.domain.slack.type.SlackSendStatus; import lombok.RequiredArgsConstructor; @@ -16,6 +27,7 @@ public class SlackMessageRepositoryImpl implements SlackMessageRepository { private final SlackMessageJpaRepository slackMessageJpaRepository; + private final JPAQueryFactory queryFactory; @Override public SlackMessage save(SlackMessage slackMessage) { @@ -28,7 +40,55 @@ public Optional findByIdAndDeletedAtIsNull(UUID slackId) { } @Override - public List findAllByDeletedAtIsNull() { - return slackMessageJpaRepository.findAllByDeletedAtIsNull(); + public Page search(SearchSlackMessageCommand command, Pageable pageable) { + + BooleanBuilder builder = new BooleanBuilder(); + builder.and(slackMessage.deletedAt.isNull()); + builder.and(receiverSlackIdEq(command.receiverSlackId())); + builder.and(sendStatusEq(command.sendStatus())); + builder.and(messageTypeEq(command.messageType())); + builder.and(createdAtGoe(command.createdAtFrom())); + builder.and(createdAtLoe(command.createdAtTo())); + + List content = queryFactory + .selectFrom(slackMessage) + .where(builder) + .orderBy(slackMessage.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long total = queryFactory + .select(slackMessage.id.count()) + .from(slackMessage) + .where(builder) + .fetchOne(); + + return new PageImpl<>(content, pageable, total == null ? 0L : total); } + + // ===== 조건 메서드 ===== + + private com.querydsl.core.types.Predicate receiverSlackIdEq(String receiverSlackId) { + return (receiverSlackId == null || receiverSlackId.isBlank()) + ? null + : slackMessage.receiverSlackId.eq(receiverSlackId); + } + + private com.querydsl.core.types.Predicate sendStatusEq(SlackSendStatus sendStatus) { + return sendStatus == null ? null : slackMessage.sendStatus.eq(sendStatus); + } + + private com.querydsl.core.types.Predicate messageTypeEq(SlackMessageType messageType) { + return messageType == null ? null : slackMessage.messageType.eq(messageType); + } + + private com.querydsl.core.types.Predicate createdAtGoe(LocalDateTime from) { + return from == null ? null : slackMessage.createdAt.goe(from); + } + + private com.querydsl.core.types.Predicate createdAtLoe(LocalDateTime to) { + return to == null ? null : slackMessage.createdAt.loe(to); + } + } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java index 0a843af..3db7102 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/dto/request/GenerateDeadlineRequest.java @@ -1,6 +1,5 @@ package com.shipflow.notificationservice.presentation.ai.dto.request; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -8,54 +7,47 @@ import com.shipflow.notificationservice.application.ai.dto.command.GenerateDeadlineCommand; import com.shipflow.notificationservice.domain.ai.type.AiRequestType; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public record GenerateDeadlineRequest( - - @NotNull(message = "relatedShipmentId는 필수입니다.") - UUID relatedShipmentId, - - @NotNull(message = "shipmentManagerId는 필수입니다.") - UUID shipmentManagerId, - - @NotBlank(message = "fromHub는 필수입니다.") - String fromHub, - - @NotBlank(message = "toHub는 필수입니다.") - String toHub, - + @NotNull UUID orderId, + @NotNull UUID relatedShipmentId, + @NotNull UUID shipmentManagerId, + @NotNull String receiverSlackId, + @NotNull UUID productId, + @NotNull String product, + @NotNull Integer quantity, + @NotNull UUID departureHubId, + @NotNull String fromHub, + @NotNull UUID arrivalHubId, + @NotNull String toHub, List route, - - @NotBlank(message = "product는 필수입니다.") - String product, - String requestNote, - - @NotNull(message = "deadline은 필수입니다.") - LocalDateTime deadline, - - String workingHours, - - @NotNull(message = "requestType은 필수입니다.") - AiRequestType requestType, - - LocalDate workDate + @NotNull LocalDateTime deadline, + String workingHours ) { - - public GenerateDeadlineCommand toCommand() { + public GenerateDeadlineCommand toCommand(UUID ordererId) { return new GenerateDeadlineCommand( + orderId, + ordererId, relatedShipmentId, shipmentManagerId, + receiverSlackId, + null, + null, + productId, + product, + quantity, + departureHubId, fromHub, + arrivalHubId, toHub, route, - product, requestNote, deadline, workingHours, - requestType, - workDate + AiRequestType.DEADLINE, + null ); } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java index 5a942ef..9bb07fd 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/ai/external/AiController.java @@ -1,19 +1,30 @@ package com.shipflow.notificationservice.presentation.ai.external; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.UUID; -import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Page; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.shipflow.common.exception.ApiResponse; +import com.shipflow.common.exception.BusinessException; import com.shipflow.notificationservice.application.ai.AiAppService; +import com.shipflow.notificationservice.application.ai.dto.command.SearchAiLogCommand; +import com.shipflow.notificationservice.domain.ai.exception.AiErrorCode; +import com.shipflow.notificationservice.domain.ai.type.AiRequestStatus; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; import com.shipflow.notificationservice.presentation.ai.dto.request.GenerateDeadlineRequest; import com.shipflow.notificationservice.presentation.ai.dto.response.AiLogResponse; +import com.shipflow.notificationservice.presentation.common.BasePageRequest; import com.shipflow.notificationservice.presentation.common.BasePageResponse; import jakarta.validation.Valid; @@ -28,33 +39,72 @@ public AiController(AiAppService aiAppService) { this.aiAppService = aiAppService; } - @PostMapping + // debug 용으로 유지 + @PostMapping("/debug") public ApiResponse generateAiLog( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, @Valid @RequestBody GenerateDeadlineRequest request ) { + + if (!"MASTER".equals(userRole)) { + throw new BusinessException(AiErrorCode.FORBIDDEN_AI_ACCESS); + } + return ApiResponse.ok( AiLogResponse.from( - aiAppService.generateAiLog(request.toCommand()) + aiAppService.generateAiLog(request.toCommand(UUID.fromString(userId))) ) ); } @GetMapping("/{aiId}") - public ApiResponse getAiLog(@PathVariable UUID aiId) { + public ApiResponse getAiLog( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @PathVariable UUID aiId + ) { return ApiResponse.ok( AiLogResponse.from( - aiAppService.getAiLog(aiId) + aiAppService.getAiLog( + UUID.fromString(userId), + userRole, + aiId + ) ) ); } @GetMapping - public ApiResponse> getAiLogs(Pageable pageable) { - return ApiResponse.ok( - BasePageResponse.from( - aiAppService.getAiLogs(pageable) - .map(AiLogResponse::from) + public ApiResponse> getAiLogs( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @RequestParam(required = false) UUID shipmentManagerId, + @RequestParam(required = false) AiRequestType requestType, + @RequestParam(required = false) AiRequestStatus requestStatus, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate workDate, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdAtFrom, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdAtTo, + BasePageRequest pageRequest + ) { + Page page = aiAppService.getAiLogs( + new SearchAiLogCommand( + UUID.fromString(userId), + userRole, + shipmentManagerId, + requestType, + requestStatus, + workDate, + createdAtFrom, + createdAtTo + ), + pageRequest.toPageable() ) - ); + .map(AiLogResponse::from); + + return ApiResponse.ok(BasePageResponse.from(page)); } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java index c735b4b..19ac97d 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/common/BasePageRequest.java @@ -5,17 +5,25 @@ import org.springframework.data.domain.Sort; public record BasePageRequest( - int page, - int size + Integer page, + Integer size ) { public BasePageRequest { - page = Math.max(page, 0); + page = (page == null || page < 0) ? 0 : page; - if (size != 10 && size != 30 && size != 50) { + if (size == null || (size != 10 && size != 30 && size != 50)) { size = 10; } } + public Pageable toPageable() { + return PageRequest.of( + page, + size, + Sort.by(Sort.Direction.DESC, "createdAt") + ); + } + public Pageable toPageable(Sort sort) { return PageRequest.of(page, size, sort); } diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java index 5b6b19a..ca9a5dc 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/SendSlackMessageRequest.java @@ -5,15 +5,30 @@ import com.shipflow.notificationservice.application.slack.dto.command.SendSlackMessageCommand; import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + public record SendSlackMessageRequest( + @NotBlank + @Pattern(regexp = "^[UCDG][A-Z0-9]+$", message = "올바른 Slack ID 형식이 아닙니다.") String receiverSlackId, + UUID relatedShipmentId, UUID relatedAiLogId, + + @NotBlank + @Size(max = 1000) String message, + + @NotNull SlackMessageType messageType ) { - public SendSlackMessageCommand toCommand() { + public SendSlackMessageCommand toCommand(UUID userId, String userRole) { return new SendSlackMessageCommand( + userId, + userRole, receiverSlackId, relatedShipmentId, relatedAiLogId, diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java index 1a7fd2b..a254220 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/request/UpdateSlackMessageRequest.java @@ -4,10 +4,19 @@ import com.shipflow.notificationservice.application.slack.dto.command.UpdateSlackMessageCommand; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + public record UpdateSlackMessageRequest( + @NotBlank + @Size(max = 1000) String message ) { - public UpdateSlackMessageCommand toCommand(UUID slackId) { - return new UpdateSlackMessageCommand(slackId, message); + public UpdateSlackMessageCommand toCommand(UUID userId, String userRole, UUID slackId) { + return new UpdateSlackMessageCommand( + userId, + userRole, + slackId, + message); } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/response/SlackMessageResponse.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/response/SlackMessageResponse.java index ad7bc30..487ff42 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/response/SlackMessageResponse.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/dto/response/SlackMessageResponse.java @@ -1,7 +1,6 @@ package com.shipflow.notificationservice.presentation.slack.dto.response; import java.time.LocalDateTime; -import java.util.List; import java.util.UUID; import com.shipflow.notificationservice.application.slack.dto.result.SlackMessageResult; @@ -35,9 +34,4 @@ public static SlackMessageResponse from(SlackMessageResult result) { ); } - public static List from(List results) { - return results.stream() - .map(SlackMessageResponse::from) - .toList(); - } } \ No newline at end of file diff --git a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java index 869d89c..65eb12d 100644 --- a/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java +++ b/notification-service/src/main/java/com/shipflow/notificationservice/presentation/slack/external/SlackController.java @@ -1,29 +1,38 @@ package com.shipflow.notificationservice.presentation.slack.external; -import java.util.List; +import java.time.LocalDateTime; import java.util.UUID; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.shipflow.common.exception.ApiResponse; import com.shipflow.notificationservice.application.slack.SlackAppService; +import com.shipflow.notificationservice.application.slack.dto.command.SearchSlackMessageCommand; +import com.shipflow.notificationservice.domain.slack.type.SlackMessageType; +import com.shipflow.notificationservice.domain.slack.type.SlackSendStatus; +import com.shipflow.notificationservice.presentation.common.BasePageResponse; import com.shipflow.notificationservice.presentation.slack.dto.request.SendSlackMessageRequest; import com.shipflow.notificationservice.presentation.slack.dto.request.UpdateSlackMessageRequest; import com.shipflow.notificationservice.presentation.slack.dto.response.SlackMessageResponse; +import jakarta.validation.Valid; + @RestController @RequestMapping("/api/slack") public class SlackController { - // TODO: Gateway + Keycloak 연동 후 @AuthenticationPrincipal 적용 - // TODO: external API 권한 체크(@PreAuthorize) 추가 - // TODO: internal API 서비스 간 인증 방식 반영 + private final SlackAppService slackAppService; public SlackController(SlackAppService slackAppService) { @@ -31,42 +40,92 @@ public SlackController(SlackAppService slackAppService) { } @PostMapping - public ApiResponse sendSlackMessage(@RequestBody SendSlackMessageRequest request) { + public ApiResponse sendSlackMessage( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @Valid @RequestBody SendSlackMessageRequest request) { return ApiResponse.ok( SlackMessageResponse.from( - slackAppService.sendSlackMessage(request.toCommand()) + slackAppService.sendSlackMessage( + request.toCommand(UUID.fromString(userId), userRole) + ) ) ); } @GetMapping("/{slackId}") - public ApiResponse getSlackMessage(@PathVariable UUID slackId) { - return ApiResponse.ok(SlackMessageResponse.from(slackAppService.getSlackMessage(slackId))); + public ApiResponse getSlackMessage( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @PathVariable UUID slackId) { + return ApiResponse.ok( + SlackMessageResponse.from( + slackAppService.getSlackMessage( + UUID.fromString(userId), + userRole, + slackId + ) + ) + ); } - // TODO: 목록 조회 페이징 및 검색 처리 필요 @GetMapping - public ApiResponse> getAllSlackMessages() { - return ApiResponse.ok(SlackMessageResponse.from(slackAppService.getSlackMessages())); + public ApiResponse> getSlackMessages( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @RequestParam(required = false) String receiverSlackId, + @RequestParam(required = false) SlackSendStatus sendStatus, + @RequestParam(required = false) SlackMessageType messageType, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdAtFrom, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdAtTo, + Pageable pageable + ) { + Page page = slackAppService.getSlackMessages( + new SearchSlackMessageCommand( + UUID.fromString(userId), + userRole, + receiverSlackId, + sendStatus, + messageType, + createdAtFrom, + createdAtTo + ), + pageable + ) + .map(SlackMessageResponse::from); + + return ApiResponse.ok(BasePageResponse.from(page)); } @PatchMapping("/{slackId}") public ApiResponse updateSlackMessage( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, @PathVariable UUID slackId, - @RequestBody UpdateSlackMessageRequest request + @Valid @RequestBody UpdateSlackMessageRequest request ) { return ApiResponse.ok( SlackMessageResponse.from( - slackAppService.updateSlackMessage(request.toCommand(slackId)) + slackAppService.updateSlackMessage( + request.toCommand(UUID.fromString(userId), userRole, slackId) + ) ) ); } @DeleteMapping("/{slackId}") - public ApiResponse deleteSlackMessage(@PathVariable UUID slackId) { - UUID userId = UUID.fromString( - "11111111-1111-1111-1111-111111111111"); // TODO: 인증 적용 후 실제 사용자 ID로 교체 (임시 system user) // TODO: Security 적용 후 교체 - slackAppService.deleteSlackMessage(slackId, userId); + public ApiResponse deleteSlackMessage( + @RequestHeader("X-User-Id") String userId, + @RequestHeader("X-User-Role") String userRole, + @PathVariable UUID slackId + ) { + slackAppService.deleteSlackMessage( + UUID.fromString(userId), + userRole, + slackId + ); return ApiResponse.ok(null); } } \ No newline at end of file diff --git a/notification-service/src/main/resources/application.yaml b/notification-service/src/main/resources/application.yaml index b09de44..e6413c5 100644 --- a/notification-service/src/main/resources/application.yaml +++ b/notification-service/src/main/resources/application.yaml @@ -1,12 +1,15 @@ spring: + main: + web-application-type: servlet + application: - name: notification-service + name: notificationservice datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?currentSchema=notification - username: ${DB_USER} - password: ${DB_PASSWORD} + url: jdbc:postgresql://${DB_HOST:postgres}:${DB_PORT:5432}/${POSTGRES_DB:shipflow}?currentSchema=notification + username: ${POSTGRES_USER:shipflow} + password: ${POSTGRES_PASSWORD} jpa: hibernate: @@ -18,8 +21,13 @@ spring: config: import: optional:file:.env + data: + redis: + host: localhost + port: 6379 + rabbitmq: - host: ${RABBITMQ_HOST:localhost} + host: ${RABBITMQ_HOST:rabbitmq} port: ${RABBITMQ_PORT:5672} username: ${RABBITMQ_USERNAME:guest} password: ${RABBITMQ_PASSWORD:guest} @@ -29,4 +37,20 @@ slack: gemini: api-key: ${GEMINI_API_KEY} - url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent \ No newline at end of file + url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + +server: + port: 8080 + +eureka: + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://discoveryserver:8761/eureka/} + +springdoc: + swagger-ui: + path: /swagger-ui.html + api-docs: + path: /v3/api-docs \ No newline at end of file diff --git a/notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java b/notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java index c69fb50..512a5c6 100644 --- a/notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java +++ b/notification-service/src/test/java/com/shipflow/notificationservice/AiAppServiceTest.java @@ -153,6 +153,8 @@ void deadline_null() { @Test void get_success() { UUID id = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; AiLog aiLog = new AiLog( UUID.randomUUID(), @@ -166,7 +168,7 @@ void get_success() { when(aiLogRepository.findByIdAndDeletedAtIsNull(id)) .thenReturn(Optional.of(aiLog)); - AiLogResult result = aiAppService.getAiLog(id); + AiLogResult result = aiAppService.getAiLog(userId, userRole, id); // 3개로 수정 assertThat(result.requestStatus()).isEqualTo(AiRequestStatus.SUCCESS); } diff --git a/notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java b/notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java new file mode 100644 index 0000000..75a6312 --- /dev/null +++ b/notification-service/src/test/java/com/shipflow/notificationservice/NotificationOrchestratorServiceTest.java @@ -0,0 +1,217 @@ +package com.shipflow.notificationservice; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.shipflow.notificationservice.application.NotificationOrchestratorService; +import com.shipflow.notificationservice.application.ai.AiAppService; +import com.shipflow.notificationservice.application.ai.dto.result.AiLogResult; +import com.shipflow.notificationservice.application.slack.SlackAppService; +import com.shipflow.notificationservice.domain.ai.AiLog; +import com.shipflow.notificationservice.domain.ai.repository.AiLogRepository; +import com.shipflow.notificationservice.domain.ai.type.AiRequestType; +import com.shipflow.notificationservice.infrastructure.client.order.OrderInternalClient; +import com.shipflow.notificationservice.infrastructure.client.order.OrderReadModelResponse; +import com.shipflow.notificationservice.infrastructure.messaging.dto.ShipmentCreatedEvent; + +@ExtendWith(MockitoExtension.class) +class NotificationOrchestratorServiceTest { + + @Mock + private AiAppService aiAppService; + @Mock + private SlackAppService slackAppService; + @Mock + private AiLogRepository aiLogRepository; + @Mock + private OrderInternalClient orderInternalClient; + + @InjectMocks + private NotificationOrchestratorService notificationOrchestratorService; + + @Test + @DisplayName("정상적인 이벤트 수신 시 슬랙 알림 발송 성공") + void handleShipmentCreated_success() { + // given + ShipmentCreatedEvent event = createEvent("U123SLACK"); + + OrderReadModelResponse order = new OrderReadModelResponse( + event.getOrderId(), + UUID.randomUUID(), // productId (UUID로 변경) + "마른 오징어", // productName + 10, // quantity + "김테스트", // ordererName + LocalDateTime.now(), // createdAt + "경기 북부 센터", // departureHubName + "부산광역시 센터", // arrivalHubName + LocalDateTime.now().plusDays(3), // requestDeadline + "빨리 보내주세요" // requestNote + ); + + AiLogResult aiResult = mock(AiLogResult.class); + when(aiResult.aiId()).thenReturn(UUID.randomUUID()); + when(aiResult.finalDeadlineAt()).thenReturn(LocalDateTime.now()); + + AiLog aiLog = new AiLog(UUID.randomUUID(), UUID.randomUUID(), "prompt", AiRequestType.DEADLINE); + aiLog.markSuccess("response", LocalDateTime.now()); + + when(orderInternalClient.getOrderReadModel(event.getOrderId())).thenReturn(order); + when(aiAppService.generateAiLog(any())).thenReturn(aiResult); + when(aiLogRepository.findByIdAndDeletedAtIsNull(any())).thenReturn(Optional.of(aiLog)); + + // when & then + assertThatNoException() + .isThrownBy(() -> notificationOrchestratorService.handleShipmentCreated(event)); + + verify(orderInternalClient).getOrderReadModel(event.getOrderId()); + verify(aiAppService).generateAiLog(any()); + verify(slackAppService).sendSlackMessage(any()); + } + + @Test + @DisplayName("order read-model 조회 실패 시 UNKNOWN으로 대체하여 진행") + void handleShipmentCreated_orderFail_continueWithUnknown() { + // given + ShipmentCreatedEvent event = createEvent("U123SLACK"); + + AiLogResult aiResult = mock(AiLogResult.class); + when(aiResult.aiId()).thenReturn(UUID.randomUUID()); + when(aiResult.finalDeadlineAt()).thenReturn(LocalDateTime.now()); + + AiLog aiLog = new AiLog(UUID.randomUUID(), UUID.randomUUID(), "prompt", AiRequestType.DEADLINE); + aiLog.markSuccess("response", LocalDateTime.now()); + + when(orderInternalClient.getOrderReadModel(any())).thenThrow(new RuntimeException("조회 실패")); + when(aiAppService.generateAiLog(any())).thenReturn(aiResult); + when(aiLogRepository.findByIdAndDeletedAtIsNull(any())).thenReturn(Optional.of(aiLog)); + + // when & then + assertThatNoException() + .isThrownBy(() -> notificationOrchestratorService.handleShipmentCreated(event)); + } + + @Test + @DisplayName("slackId가 null이면 UNKNOWN으로 대체") + void handleShipmentCreated_nullSlackId() { + // given + ShipmentCreatedEvent event = createEvent(null); // slackId null + + OrderReadModelResponse order = new OrderReadModelResponse( + event.getOrderId(), + UUID.randomUUID(), // productId (UUID로 변경) + "마른 오징어", // productName + 10, // quantity + "김테스트", // ordererName + LocalDateTime.now(), // createdAt + "경기 북부 센터", // departureHubName + "부산광역시 센터", // arrivalHubName + LocalDateTime.now().plusDays(3), // requestDeadline + "빨리 보내주세요" // requestNote + ); + + AiLogResult aiResult = mock(AiLogResult.class); + when(aiResult.aiId()).thenReturn(UUID.randomUUID()); + when(aiResult.finalDeadlineAt()).thenReturn(LocalDateTime.now()); + + AiLog aiLog = new AiLog(UUID.randomUUID(), UUID.randomUUID(), "prompt", AiRequestType.DEADLINE); + aiLog.markSuccess("response", LocalDateTime.now()); + + when(orderInternalClient.getOrderReadModel(any())).thenReturn(order); + when(aiAppService.generateAiLog(any())).thenReturn(aiResult); + when(aiLogRepository.findByIdAndDeletedAtIsNull(any())).thenReturn(Optional.of(aiLog)); + + // when & then - UNKNOWN으로 대체되어 진행됨 + assertThatNoException() + .isThrownBy(() -> notificationOrchestratorService.handleShipmentCreated(event)); + } + + @Test + @DisplayName("경유지가 있으면 sequence 기반 경유 텍스트 생성") + void handleShipmentCreated_withRoutes() { + // given + ShipmentCreatedEvent event = createEventWithRoutes("U123SLACK"); + + OrderReadModelResponse order = new OrderReadModelResponse( + event.getOrderId(), + UUID.randomUUID(), // productId (UUID로 변경) + "마른 오징어", // productName + 10, // quantity + "김테스트", // ordererName + LocalDateTime.now(), // createdAt + "경기 북부 센터", // departureHubName + "부산광역시 센터", // arrivalHubName + LocalDateTime.now().plusDays(3), // requestDeadline + "빨리 보내주세요" // requestNote + ); + + AiLogResult aiResult = mock(AiLogResult.class); + when(aiResult.aiId()).thenReturn(UUID.randomUUID()); + when(aiResult.finalDeadlineAt()).thenReturn(LocalDateTime.now()); + + AiLog aiLog = new AiLog(UUID.randomUUID(), UUID.randomUUID(), "prompt", AiRequestType.DEADLINE); + aiLog.markSuccess("response", LocalDateTime.now()); + + when(orderInternalClient.getOrderReadModel(any())).thenReturn(order); + when(aiAppService.generateAiLog(any())).thenReturn(aiResult); + when(aiLogRepository.findByIdAndDeletedAtIsNull(any())).thenReturn(Optional.of(aiLog)); + + // when & then + assertThatNoException() + .isThrownBy(() -> notificationOrchestratorService.handleShipmentCreated(event)); + + verify(aiAppService).generateAiLog(argThat(command -> + command.route().contains("1번 경유") && command.route().contains("2번 경유") + )); + } + + // ── 픽스처 ────────────────────────────────────── + + private ShipmentCreatedEvent createEvent(String slackId) { + // reflection으로 필드 세팅 (getter only라서) + ShipmentCreatedEvent event = new ShipmentCreatedEvent( + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + 10, + UUID.randomUUID(), + UUID.randomUUID(), + LocalDateTime.now().plusDays(3), + "빨리 보내주세요", + slackId, + null + ); + return event; + } + + private ShipmentCreatedEvent createEventWithRoutes(String slackId) { + ShipmentCreatedEvent event = new ShipmentCreatedEvent( + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + 10, + UUID.randomUUID(), + UUID.randomUUID(), + LocalDateTime.now().plusDays(3), + "빨리 보내주세요", + slackId, + List.of( + new ShipmentCreatedEvent.RouteInfo(1, UUID.randomUUID(), UUID.randomUUID()), + new ShipmentCreatedEvent.RouteInfo(2, UUID.randomUUID(), UUID.randomUUID()) + ) + ); + return event; + } +} \ No newline at end of file diff --git a/notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java b/notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java index 0a6dc64..6faeb04 100644 --- a/notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java +++ b/notification-service/src/test/java/com/shipflow/notificationservice/SlackAppServiceTest.java @@ -15,9 +15,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import com.shipflow.common.exception.BusinessException; import com.shipflow.notificationservice.application.slack.SlackAppService; +import com.shipflow.notificationservice.application.slack.dto.command.SearchSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.command.SendSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.command.UpdateSlackMessageCommand; import com.shipflow.notificationservice.application.slack.dto.result.SlackMessageResult; @@ -51,10 +56,14 @@ class SendSlackMessageTest { @DisplayName("수동 발송에 성공하면 SUCCESS 상태로 저장") void sendSlackMessage_success() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID relatedShipmentId = UUID.randomUUID(); UUID relatedAiLogId = UUID.randomUUID(); SendSlackMessageCommand command = new SendSlackMessageCommand( + userId, + userRole, "U0APZGV2NRH", relatedShipmentId, relatedAiLogId, @@ -98,10 +107,14 @@ void sendSlackMessage_success() { @DisplayName("슬랙 전송에 실패하면 FAIL 상태로 저장") void sendSlackMessage_fail() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID relatedShipmentId = UUID.randomUUID(); UUID relatedAiLogId = UUID.randomUUID(); SendSlackMessageCommand command = new SendSlackMessageCommand( + userId, + userRole, "U0APZGV2NRH", relatedShipmentId, relatedAiLogId, @@ -138,6 +151,8 @@ class GetSlackMessageTest { @DisplayName("존재하는 메시지 단건 조회 성공") void getSlackMessage_success() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID slackId = UUID.randomUUID(); SlackMessage slackMessage = new SlackMessage( @@ -153,7 +168,7 @@ void getSlackMessage_success() { .thenReturn(Optional.of(slackMessage)); // when - SlackMessageResult result = slackAppService.getSlackMessage(slackId); + SlackMessageResult result = slackAppService.getSlackMessage(userId, userRole, slackId); // then assertThat(result.receiverSlackId()).isEqualTo("U0APZGV2NRH"); @@ -165,13 +180,15 @@ void getSlackMessage_success() { @DisplayName("존재하지 않는 메시지 조회 시 BusinessException 발생") void getSlackMessage_notFound() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID slackId = UUID.randomUUID(); when(slackMessageRepository.findByIdAndDeletedAtIsNull(slackId)) .thenReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> slackAppService.getSlackMessage(slackId)) + assertThatThrownBy(() -> slackAppService.getSlackMessage(userId, userRole, slackId)) .isInstanceOf(BusinessException.class); } } @@ -184,6 +201,9 @@ class GetSlackMessagesTest { @DisplayName("삭제되지 않은 메시지 목록 전체 조회") void getSlackMessages_success() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; + SlackMessage first = new SlackMessage( "U0APZGV2NRH", UUID.randomUUID(), @@ -202,18 +222,23 @@ void getSlackMessages_success() { ); second.markFail(); - when(slackMessageRepository.findAllByDeletedAtIsNull()) - .thenReturn(List.of(first, second)); + Pageable pageable = PageRequest.of(0, 10); + SearchSlackMessageCommand command = new SearchSlackMessageCommand( + userId, userRole, null, null, null, null, null + ); + + when(slackMessageRepository.search(any(SearchSlackMessageCommand.class), any(Pageable.class))) + .thenReturn(new PageImpl<>(List.of(first, second), pageable, 2)); // when - List results = slackAppService.getSlackMessages(); + Page results = slackAppService.getSlackMessages(command, pageable); // then - assertThat(results).hasSize(2); - assertThat(results.get(0).message()).isEqualTo("첫 번째 메시지"); - assertThat(results.get(0).sendStatus()).isEqualTo(SlackSendStatus.SUCCESS); - assertThat(results.get(1).message()).isEqualTo("두 번째 메시지"); - assertThat(results.get(1).sendStatus()).isEqualTo(SlackSendStatus.FAIL); + assertThat(results.getContent()).hasSize(2); + assertThat(results.getContent().get(0).message()).isEqualTo("첫 번째 메시지"); + assertThat(results.getContent().get(0).sendStatus()).isEqualTo(SlackSendStatus.SUCCESS); + assertThat(results.getContent().get(1).message()).isEqualTo("두 번째 메시지"); + assertThat(results.getContent().get(1).sendStatus()).isEqualTo(SlackSendStatus.FAIL); } } @@ -225,9 +250,13 @@ class UpdateSlackMessageTest { @DisplayName("SUCCESS 상태의 메시지는 수정 가능") void updateSlackMessage_success() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID slackId = UUID.randomUUID(); UpdateSlackMessageCommand command = new UpdateSlackMessageCommand( + userId, + userRole, slackId, "수정된 메시지" ); @@ -264,9 +293,13 @@ void updateSlackMessage_success() { @DisplayName("존재하지 않는 메시지 수정 시 BusinessException 발생") void updateSlackMessage_notFound() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID slackId = UUID.randomUUID(); UpdateSlackMessageCommand command = new UpdateSlackMessageCommand( + userId, + userRole, slackId, "수정된 메시지" ); @@ -285,9 +318,13 @@ void updateSlackMessage_notFound() { @DisplayName("아직 발송되지 않은(slackTs 없는) 메시지는 수정 불가") void updateSlackMessage_notSent() { // given + UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; UUID slackId = UUID.randomUUID(); UpdateSlackMessageCommand command = new UpdateSlackMessageCommand( + userId, + userRole, slackId, "수정된 메시지" ); @@ -322,6 +359,7 @@ void deleteSlackMessage_success() { // given UUID slackId = UUID.randomUUID(); UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; SlackMessage slackMessage = new SlackMessage( "U0APZGV2NRH", @@ -342,7 +380,7 @@ void deleteSlackMessage_success() { )); // when - slackAppService.deleteSlackMessage(slackId, userId); + slackAppService.deleteSlackMessage(userId, userRole, slackId); // then verify(slackSender).deleteMessage("C0AQ2G43EUD", "1742891400.123456"); @@ -356,12 +394,13 @@ void deleteSlackMessage_notFound() { // given UUID slackId = UUID.randomUUID(); UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; when(slackMessageRepository.findByIdAndDeletedAtIsNull(slackId)) .thenReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> slackAppService.deleteSlackMessage(slackId, userId)) + assertThatThrownBy(() -> slackAppService.deleteSlackMessage(userId, userRole, slackId)) .isInstanceOf(BusinessException.class); verify(slackSender, never()).deleteMessage(any(), any()); @@ -373,6 +412,7 @@ void deleteSlackMessage_failStatus() { // given UUID slackId = UUID.randomUUID(); UUID userId = UUID.randomUUID(); + String userRole = "MASTER"; SlackMessage slackMessage = new SlackMessage( "U0APZGV2NRH", @@ -391,7 +431,7 @@ void deleteSlackMessage_failStatus() { // null channelId/ts로 deleteMessage가 호출될 수 있음 → 실제 동작 확인 후 조정 // when & then - assertThatThrownBy(() -> slackAppService.deleteSlackMessage(slackId, userId)) + assertThatThrownBy(() -> slackAppService.deleteSlackMessage(userId, userRole, slackId)) .isInstanceOf(BusinessException.class); verify(slackSender, never()).deleteMessage(any(), any());