├── .dockerignore ├── .gitattributes ├── .github └── workflows │ ├── app.yml │ ├── publish.yml │ └── sonarcloud.yml ├── .gitignore ├── .gitmodules ├── .husky ├── commit-msg └── pre-commit ├── .java-version ├── .npmrc ├── .nvmrc ├── .watchmanconfig ├── .yarnrc ├── Dockerfile ├── LICENSE ├── README.md ├── app.code-workspace ├── babel.config.js ├── bin ├── curltest.sh ├── extract.sh ├── jmeter.sh ├── loadtest-ludicrous.sh └── loadtest.sh ├── build-prometheus.sh ├── build-service.sh ├── build.gradle ├── build.sh ├── buildSrc ├── build.gradle └── src │ └── main │ └── groovy │ └── net.trajano.swarm.conventions.gradle ├── commitlint.config.js ├── deploy.sh ├── docker-compose-deploy-test.yml ├── docker-compose-deploy.yml ├── docker-compose.yml ├── dynamic-grpc-client ├── .gitignore ├── README.md ├── build.gradle └── src │ └── main │ ├── java │ └── net │ │ └── trajano │ │ └── swarm │ │ └── grpc │ │ ├── Client.java │ │ └── GrpcMessageContext.java │ └── resources │ └── application.properties ├── expo-app ├── .expo-shared │ ├── README.md │ └── assets.json ├── .gitignore ├── .vscode │ └── settings.json ├── README.md ├── app.config.ts ├── app.json ├── assets │ ├── fonts │ │ └── SpaceMono-Regular.ttf │ ├── images │ │ ├── adaptive-icon.png │ │ ├── dev-client-icon.png │ │ ├── favicon.png │ │ ├── icon.png │ │ └── splash.png │ └── lottie │ │ └── 28839-ikura-sushi.json ├── authenticated-context │ ├── AuthenticatedProvider.tsx │ ├── IAuthenticated.ts │ ├── IAuthenticatedContext.tsx │ ├── JwtClaims.ts │ ├── actions.ts │ ├── index.ts │ ├── jwtVerify.ts │ ├── jwtVerify.web.ts │ ├── reducers.ts │ ├── sseEventSlice.ts │ ├── useAuthenticated.ts │ ├── useDb.ts │ └── useDb.web.ts ├── babel.config.js ├── constants │ ├── Colors.ts │ └── Layout.ts ├── eas.json ├── eas.sh ├── easupdate.sh ├── env.d.ts ├── metro.config.js ├── navigation │ ├── LinkingConfiguration.ts │ ├── index.tsx │ ├── login │ │ ├── LoginNavigator.tsx │ │ ├── LoginScreen.tsx │ │ └── types.ts │ └── paramLists.ts ├── package.json ├── screens │ ├── AsyncStorageScreen.tsx │ ├── EnvironmentScreen.tsx │ ├── ExpoUpdateScreen.tsx │ ├── JustScrollView.tsx │ ├── LoadingScreen.tsx │ ├── MainDrawer.tsx │ ├── ModalScreen.tsx │ ├── NetworkLoggerScreen.tsx │ ├── NetworkLoggerScreen.web.tsx │ ├── NetworkLoggerTab.tsx │ ├── NotFoundScreen.tsx │ ├── OneViewScreen.tsx │ ├── StackNavigatorScrollView │ │ ├── StackNavigatorScrollView.tsx │ │ ├── StackNavigatorScrollViewScreen.tsx │ │ └── index.ts │ ├── SystemFontScreen.test.tsx │ ├── SystemFontsScreen.tsx │ ├── TabOneNavigator.tsx │ ├── TabOneScreen.tsx │ ├── TabTwoScreen.tsx │ ├── TextTab.tsx │ └── TextTest.tsx ├── src │ ├── __tests__ │ │ ├── crypto.test.ts │ │ ├── env.test.ts │ │ ├── jsxsanity.test.tsx │ │ ├── sanity.test.ts │ │ └── useColorScheme.test.ts │ ├── app-context │ │ ├── AppContext.ts │ │ ├── AppProvider.tsx │ │ ├── AppProviderProps.ts │ │ ├── IAppContext.ts │ │ ├── default.ts │ │ ├── index.ts │ │ ├── useApp.ts │ │ ├── useBackendReachable.ts │ │ └── useLastAuthEvents.ts │ ├── components │ │ ├── Button.tsx │ │ ├── ErrorView │ │ │ ├── ErrorView.tsx │ │ │ ├── SimpleErrorView.tsx │ │ │ ├── StackTraceErrorView.tsx │ │ │ └── index.ts │ │ └── Text.tsx │ ├── header-test │ │ ├── HeaderDxOneViewScreen.tsx │ │ ├── HeaderDxStackNavigation.tsx │ │ ├── HeaderTestNavigationContainer.tsx │ │ ├── MainTabNavigation.tsx │ │ ├── NativeOneViewScreen.tsx │ │ ├── NativeStackNavigation.tsx │ │ ├── OneViewContent.tsx │ │ └── index.ts │ ├── hooks │ │ ├── __mocks__ │ │ │ └── @react-native-async-storage │ │ │ │ └── async-storage.ts │ │ ├── millisecondsBeforeNextDay.test.ts │ │ ├── useCachedResources.ts │ │ ├── useColorScheme.ts │ │ ├── useDayClockState.ts │ │ ├── useExpoUpdateEffect.ts │ │ ├── useShakeEffect.ts │ │ ├── useStoredState.test.ts │ │ └── useStoredState.ts │ ├── i18n │ │ ├── en-CA.json │ │ ├── en-US.json │ │ ├── en.json │ │ └── ja.json │ ├── init │ │ ├── App.tsx │ │ ├── hideSplash.android.ts │ │ ├── hideSplash.ios.ts │ │ ├── hideSplash.ts │ │ ├── index.ts │ │ ├── initialize.web.ts │ │ ├── layout-animation.android.ts │ │ ├── layout-animation.ts │ │ ├── sanitize-unhandled-promise-exceptions.ts │ │ ├── start-network-logging.ts │ │ └── useStoredEndpointConfigurationEffect.ts │ └── lib │ │ ├── app-loading │ │ ├── AppLoading.tsx │ │ ├── AppLoadingProps.ts │ │ ├── LoadingComponentProps.ts │ │ ├── README.md │ │ └── index.ts │ │ ├── app-log │ │ ├── AppEvent.ts │ │ ├── AppLogContext.tsx │ │ ├── LoggedAuthEvent.ts │ │ └── index.ts │ │ ├── native-unstyled │ │ ├── ColorSchemeColors.ts │ │ ├── ColorSwatch.ts │ │ ├── Fonts.test.tsx │ │ ├── I18n.test.tsx │ │ ├── ITheme.tsx │ │ ├── README.md │ │ ├── StatusBar.tsx │ │ ├── StyleProps.tsx │ │ ├── StyledProps.ts │ │ ├── TextStyleProps.tsx │ │ ├── Theme.test.tsx │ │ ├── ThemeContext.tsx │ │ ├── ThemeProviderProps.tsx │ │ ├── ThemedPressable.tsx │ │ ├── Themes.ts │ │ ├── Typography.tsx │ │ ├── __mocks__ │ │ │ └── expo-font.ts │ │ ├── components.ts │ │ ├── defaultColorSchemes │ │ │ ├── defaultDarkColorSchemeColors.ts │ │ │ ├── defaultLightColorSchemeColors.ts │ │ │ └── index.ts │ │ ├── defaultFontTheme.ts │ │ ├── hoc │ │ │ ├── HocOptions.ts │ │ │ ├── InputState.tsx │ │ │ ├── doStyleWrap.tsx │ │ │ ├── hoc.test.tsx │ │ │ ├── hocDisplayName.ts │ │ │ ├── index.ts │ │ │ ├── withI18n.tsx │ │ │ ├── withReplacedWithNativeFonts.tsx │ │ │ ├── withStyled.tsx │ │ │ ├── withStyledScrollView.ios.tsx │ │ │ ├── withStyledScrollView.tsx │ │ │ ├── withStyledSwitch.tsx │ │ │ ├── withStyledText.tsx │ │ │ ├── withStyledTextInput.tsx │ │ │ └── withTextRole.tsx │ │ ├── index.ts │ │ ├── lookupColor.test.ts │ │ ├── lookupColor.ts │ │ ├── markdownToTextElements.test.tsx │ │ ├── markdownToTextElements.ts │ │ ├── propsToStyleSheet.tsx │ │ ├── replaceStyleWithNativeFont.test.ts │ │ ├── replaceStyleWithNativeFont.tsx │ │ ├── swatches │ │ │ └── index.ts │ │ ├── useAlert.tsx │ │ ├── useConfiguredColorScheme.ts │ │ ├── useConfiguredLocale.test.ts │ │ ├── useConfiguredLocale.ts │ │ ├── useExpoFonts.test.ts │ │ ├── useExpoFonts.ts │ │ └── useRefreshControl.tsx │ │ ├── opinionated-design │ │ └── README.md │ │ └── stack-navigator-header-dx │ │ ├── HeaderDx.tsx │ │ ├── HeaderDxContext.tsx │ │ ├── HeaderDxLarge.tsx │ │ ├── NonNativeStackView.tsx │ │ └── index.ts ├── tsconfig.json ├── types.tsx └── webpack.config.js ├── gateway-common ├── .gitignore ├── README.md ├── build.gradle └── src │ ├── main │ ├── java │ │ └── net │ │ │ └── trajano │ │ │ └── swarm │ │ │ └── gateway │ │ │ ├── common │ │ │ └── AuthProperties.java │ │ │ ├── converters │ │ │ ├── Converters.java │ │ │ ├── JsonWebKeySetToStringConverter.java │ │ │ ├── JsonWebKeyToStringConverter.java │ │ │ ├── JwtClaimsToStringConverter.java │ │ │ ├── StringToJsonWebKeyConverter.java │ │ │ ├── StringToJsonWebKeySetConverter.java │ │ │ └── StringToJwtClaimsConverter.java │ │ │ ├── healthcheck │ │ │ ├── HealthProbe.java │ │ │ └── package-info.java │ │ │ └── redis │ │ │ ├── RedisKeyBlocks.java │ │ │ ├── UserSession.java │ │ │ └── package-info.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── net │ └── trajano │ └── swarm │ └── gateway │ └── common │ └── RedisBlockTests.java ├── gateway ├── .gitignore ├── build.gradle └── src │ ├── main │ ├── java │ │ └── net │ │ │ └── trajano │ │ │ └── swarm │ │ │ └── gateway │ │ │ ├── CircuitBreakerLogger.java │ │ │ ├── DelegateScheduleExecutorService.java │ │ │ ├── ExcludedPathPatterns.java │ │ │ ├── GatewayApplication.java │ │ │ ├── LoggingThreadFactory.java │ │ │ ├── ObservabilityConfiguration.java │ │ │ ├── SchedulerConfiguration.java │ │ │ ├── ServerWebExchangeAttributes.java │ │ │ ├── TracingProvider.java │ │ │ ├── auth │ │ │ ├── AbstractAuthController.java │ │ │ ├── AuthControllerMappingsProperties.java │ │ │ ├── AuthCredentialStorage.java │ │ │ ├── AuthServiceResponse.java │ │ │ ├── AuthenticationContext.java │ │ │ ├── IdentityService.java │ │ │ ├── IdentityServiceResponse.java │ │ │ ├── OAuthRefreshRequest.java │ │ │ ├── OAuthRevocationRequest.java │ │ │ ├── OAuthTokenResponse.java │ │ │ ├── ProtectedResourceGatewayFilterFactory.java │ │ │ ├── claims │ │ │ │ ├── ClaimsService.java │ │ │ │ ├── JwtFunctions.java │ │ │ │ └── ZLibStringCompression.java │ │ │ ├── clientmanagement │ │ │ │ ├── ClientManagementConfiguration.java │ │ │ │ ├── ClientManagementService.java │ │ │ │ ├── ClientValidGatewayFilterFactory.java │ │ │ │ ├── InvalidClientException.java │ │ │ │ └── NoCheckClientManagementService.java │ │ │ ├── oidc │ │ │ │ ├── ReactiveOidcService.java │ │ │ │ ├── WellKnownOpenIdConfiguration.java │ │ │ │ ├── WellKnownReactiveOidcService.java │ │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ └── simple │ │ │ │ ├── AuthenticationItem.java │ │ │ │ ├── SimpleAuthController.java │ │ │ │ ├── SimpleAuthServiceConfiguration.java │ │ │ │ ├── SimpleAuthServiceProperties.java │ │ │ │ ├── SimpleAuthenticationRequest.java │ │ │ │ ├── SimpleIdentityService.java │ │ │ │ └── package-info.java │ │ │ ├── datasource │ │ │ └── redis │ │ │ │ ├── IncrRedisReactiveHealthIndicator.java │ │ │ │ ├── RedisClaimsService.java │ │ │ │ ├── RedisJtiExtractorService.java │ │ │ │ ├── RedisJwksProvider.java │ │ │ │ ├── RedisStoreAndSignIdentityService.java │ │ │ │ ├── RedisUserSessions.java │ │ │ │ └── RefreshContext.java │ │ │ ├── discovery │ │ │ ├── ContainerServiceInstance.java │ │ │ ├── DockerDiscoveryConfiguration.java │ │ │ ├── DockerDiscoveryProperties.java │ │ │ ├── DockerEventWatcher.java │ │ │ ├── DockerReactiveDiscoveryClient.java │ │ │ ├── DockerServiceInstance.java │ │ │ ├── DockerServiceInstanceBuilder.java │ │ │ ├── DockerServiceInstanceLister.java │ │ │ ├── Util.java │ │ │ └── ratelimiter │ │ │ │ ├── DiscoveryRequestRateLimiterGatewayFilterFactory.java │ │ │ │ └── RateLimiterConfiguration.java │ │ │ ├── docker │ │ │ ├── DockerClientProvider.java │ │ │ ├── DockerProperties.java │ │ │ └── ReactiveDockerClient.java │ │ │ ├── grpc │ │ │ ├── ChannelProvider.java │ │ │ ├── DataBufferFluxInputStream.java │ │ │ ├── GrpcGatewayFilterFactory.java │ │ │ ├── GrpcHealthIndicator.java │ │ │ ├── GrpcServerReflection.java │ │ │ ├── JwtCallCredentials.java │ │ │ ├── MethodDescriptorCacheKey.java │ │ │ ├── ServerSentEventFunctions.java │ │ │ ├── ServerStreamingGrpcGlobalFilter.java │ │ │ └── UnaryGrpcGlobalFilter.java │ │ │ ├── jwks │ │ │ ├── JwksHealthIndicator.java │ │ │ └── JwksProvider.java │ │ │ ├── metrics │ │ │ └── CounterProvider.java │ │ │ ├── perf │ │ │ ├── PerformanceLoggingRunnable.java │ │ │ ├── PerformanceRequestIDGlobalFilter.java │ │ │ ├── PerformanceRequestIDPostFilter.java │ │ │ └── TracingHeaderPreFilter.java │ │ │ └── web │ │ │ ├── CoreRoutes.java │ │ │ ├── CorsConfigurer.java │ │ │ ├── CustomErrorWebExceptionHandler.java │ │ │ ├── GatewayResponse.java │ │ │ ├── InvalidClientGatewayResponse.java │ │ │ ├── OrderedGlobalFilter.java │ │ │ ├── StarStarRedirectGatewayFilterFactory.java │ │ │ └── UnauthorizedGatewayResponse.java │ └── resources │ │ ├── application-test.yml │ │ ├── application.yml │ │ └── static │ │ ├── favicon.ico │ │ └── robots.txt │ └── test │ ├── artillery │ ├── functional-test.yml │ ├── happy-path-no-grpc.yml │ ├── happy-path.yml │ ├── kill-ping.yml │ └── timeout-test.yml │ ├── java │ └── net │ │ └── trajano │ │ └── swarm │ │ └── gateway │ │ ├── ContainerTests.java │ │ ├── EcCryptoTests.java │ │ ├── ExecutionTimeTest.java │ │ ├── FluxPoolTest.java │ │ ├── GatewayApplicationTests.java │ │ ├── InfiniteFluxTest.java │ │ ├── RsaCryptoTests.java │ │ ├── SpelTests.java │ │ ├── TestContainerTests.java │ │ ├── auth │ │ └── simple │ │ │ ├── RedisAuthCacheTest.java │ │ │ ├── SimpleAuthControllerTest.java │ │ │ ├── UriTest.java │ │ │ └── ZLibStringCompressionTest.java │ │ ├── datasource │ │ └── redis │ │ │ └── RedisJtiExtractorServiceTest.java │ │ ├── discovery │ │ └── UtilTest.java │ │ └── jwks │ │ └── DatabaseJwksProviderTest.java │ ├── jmeter │ ├── functional-test.jmx │ ├── happy-path.jmx │ └── load-test.jmx │ └── resources │ └── logback-test.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── greclipse.prefs ├── grpc-service ├── .gitignore ├── README.md ├── build.gradle └── src │ ├── main │ ├── java │ │ └── net │ │ │ └── trajano │ │ │ └── swarm │ │ │ └── sampleservice │ │ │ ├── EchoService.java │ │ │ ├── GrpcServer.java │ │ │ ├── HealthProbe.java │ │ │ └── SampleServiceApplication.java │ ├── proto │ │ ├── chat.proto │ │ ├── echo.proto │ │ ├── import1.proto │ │ └── import2.proto │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── net │ └── trajano │ └── swarm │ └── sampleservice │ ├── EchoServiceTest.java │ └── SampleServiceApplicationTests.java ├── jwks-provider ├── .gitignore ├── build.gradle └── src │ ├── main │ ├── java │ │ └── net │ │ │ └── trajano │ │ │ └── swarm │ │ │ └── jwksprovider │ │ │ ├── Blocks.java │ │ │ ├── CryptoProvider.java │ │ │ ├── JsonWebKeyPairProvider.java │ │ │ ├── JwkProviderApplication.java │ │ │ ├── ObservabilityConfiguration.java │ │ │ └── redis │ │ │ ├── IncrRedisReactiveHealthIndicator.java │ │ │ ├── JwksRedisPopulator.java │ │ │ ├── RedisBlocksController.java │ │ │ ├── Scheduler.java │ │ │ └── UserSessionCleaner.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── net │ └── trajano │ └── swarm │ └── jwksprovider │ └── JwkProviderApplicationTests.java ├── logging ├── .gitignore ├── build.gradle └── src │ └── main │ ├── java │ └── net │ │ └── trajano │ │ └── swarm │ │ └── logging │ │ └── autoconfig │ │ └── TraceEnvironmentPostProcessor.java │ └── resources │ ├── META-INF │ └── spring.factories │ └── logback-spring.xml ├── package.json ├── packages ├── auth-context │ ├── .circleci │ │ └── config.yml │ ├── .editorconfig │ ├── .gitattributes │ ├── .gitignore │ ├── .watchmanconfig │ ├── .yarnrc │ ├── AuthState copy.puml │ ├── AuthState.puml │ ├── README.md │ ├── babel.config.js │ ├── example │ │ ├── .expo-shared │ │ │ └── assets.json │ │ ├── App.ts │ │ ├── app.json │ │ ├── assets │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── icon.png │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── metro.config.js │ │ ├── package.json │ │ ├── src │ │ │ └── App.tsx │ │ ├── tsconfig.json │ │ ├── webpack.config.js │ │ └── yarn.lock │ ├── package.json │ ├── scripts │ │ └── bootstrap.js │ ├── src │ │ ├── .eslintrc │ │ ├── AuthClient.ts │ │ ├── AuthContext.test.tsx │ │ ├── AuthContext.tsx │ │ ├── AuthEvent.ts │ │ ├── AuthProvider │ │ │ ├── AuthProvider.test.tsx │ │ │ ├── AuthProvider.tsx │ │ │ ├── AuthProviderProps.tsx │ │ │ ├── AuthProviderRefresh.test.tsx │ │ │ ├── AuthProviderRestore.test.tsx │ │ │ ├── InternalProviderState.ts │ │ │ ├── index.ts │ │ │ ├── isTokenExpired.test.ts │ │ │ ├── isTokenExpired.ts │ │ │ ├── timeToNextExpirationCheck.test.ts │ │ │ ├── timeToNextExpirationCheck.ts │ │ │ ├── useAppStateWithNetInfoRefresh.test.ts │ │ │ ├── useAppStateWithNetInfoRefresh.ts │ │ │ ├── useInitialAuthStateEffect.ts │ │ │ ├── useNoTokenAvailableEffect │ │ │ │ ├── index.ts │ │ │ │ ├── useNoTokenAvailableEffect.ts │ │ │ │ ├── useUnauthenticatedOfflineStateEffect.ts │ │ │ │ └── useUnauthenticatedStateEffect.ts │ │ │ ├── useRenderOnTokenEvent.test.ts │ │ │ ├── useRenderOnTokenEvent.ts │ │ │ └── useTokenAvailableEffect │ │ │ │ ├── index.ts │ │ │ │ ├── useAuthenticatedStateEffect.ts │ │ │ │ ├── useBackendFailureStateEffect.ts │ │ │ │ ├── useBackendInaccessibleStateEffect.test.ts │ │ │ │ ├── useBackendInaccessibleStateEffect.ts │ │ │ │ ├── useDispatchingStateEffect.ts │ │ │ │ ├── useRefreshingStateEffect.ts │ │ │ │ ├── useRestoringStateEffect.ts │ │ │ │ ├── useTokenAvailableEffect.ts │ │ │ │ ├── useTokenRemovalState.ts │ │ │ │ ├── useUsableTokenStateEffect.test.ts │ │ │ │ └── useUsableTokenStateEffect.ts │ │ ├── AuthState.ts │ │ ├── AuthStore │ │ │ ├── AuthStore.test.ts │ │ │ ├── AuthStore.ts │ │ │ ├── IAuthStore.ts │ │ │ ├── index.ts │ │ │ ├── isValidOAuthToken.test.ts │ │ │ └── isValidOAuthToken.ts │ │ ├── AuthenticationClientError.ts │ │ ├── EndpointConfiguration.ts │ │ ├── IAuth.ts │ │ ├── OAuthToken.ts │ │ ├── __mocks__ │ │ │ ├── @react-native-async-storage │ │ │ │ └── async-storage.ts │ │ │ ├── @react-native-community │ │ │ │ └── netinfo │ │ │ │ │ └── index.ts │ │ │ └── react-native │ │ │ │ └── Libraries │ │ │ │ └── Animated │ │ │ │ └── NativeAnimatedHelper.ts │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── testing-library.test.tsx.snap │ │ │ ├── fetch.test.tsx │ │ │ ├── hookEffect.test.tsx │ │ │ ├── netinfoMock.test.tsx │ │ │ └── testing-library.test.tsx │ │ ├── basicAuthorization.test.ts │ │ ├── basicAuthorization.ts │ │ ├── buildSimpleEndpointConfiguration.test.ts │ │ ├── buildSimpleEndpointConfiguration.ts │ │ ├── index.test.tsx │ │ ├── index.ts │ │ ├── useAppActiveState.ts │ │ ├── useAuth.ts │ │ ├── useBackendReachable.test.ts │ │ ├── useBackendReachable.ts │ │ ├── useNetInfoState │ │ │ ├── index.ts │ │ │ ├── netInfoStateReducer.ts │ │ │ ├── useNetInfoState.test.ts │ │ │ └── useNetInfoState.ts │ │ └── validateEndpointConfiguration.ts │ ├── tsconfig.build.json │ └── tsconfig.json └── eslint-config │ ├── README.md │ ├── index.js │ └── package.json ├── prometheus.yml ├── react-app ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts └── tsconfig.json ├── redocly.yaml ├── reyarn.sh ├── sample-service ├── .gitignore ├── README.md ├── build.gradle └── src │ ├── main │ ├── java │ │ └── net │ │ │ └── trajano │ │ │ └── swarm │ │ │ └── sampleservice │ │ │ ├── CounterProvider.java │ │ │ ├── EmployeeController.java │ │ │ ├── GrpcController.java │ │ │ ├── GrpcServer.java │ │ │ ├── GrpcServiceMethod.java │ │ │ ├── SampleServiceApplication.java │ │ │ └── echo │ │ │ ├── EchoController.java │ │ │ ├── EchoRequest.java │ │ │ └── EchoResponse.java │ ├── proto │ │ ├── chat.proto │ │ └── echo.proto │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── net │ └── trajano │ └── swarm │ └── sampleservice │ └── SampleServiceApplicationTests.java ├── settings.gradle ├── spring-redis-region ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── net │ └── trajano │ └── swarm │ └── spring │ └── redisregion │ ├── SpringRedisRegionFactory.java │ ├── SpringRedisStorageAccess.java │ └── package-info.java ├── src ├── openapi │ ├── auth.yaml │ ├── components.yaml │ ├── simple-auth.yaml │ ├── spring-cloud-docker-swarm.yaml │ └── whoami.yaml └── puml │ ├── gateway-redis.puml │ ├── grpc.puml │ └── traefik-middlware.puml └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | **/.gradle/ 3 | **/build/ 4 | gradlew 5 | gradlew.bat 6 | .git* 7 | .git/ 8 | README.md 9 | .idea/ 10 | *.iml 11 | *.ipr 12 | *.iws 13 | docker-compose.yml 14 | gateway/src/test/artillery/* 15 | **/node_modules/ 16 | gateway/src/test/java/net/trajano/swarm/gateway/ContainerTests.java 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | *.sh text eol=lf 7 | *.ts text eol=lf 8 | *.tsx text eol=lf 9 | *.json text eol=lf 10 | *.js text eol=lf 11 | *.jsx text eol=lf 12 | .husky/ text eol=lf 13 | *.gradle text eol=lf 14 | -------------------------------------------------------------------------------- /.github/workflows/app.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - rework 5 | 6 | jobs: 7 | 8 | app: 9 | runs-on: ubuntu-latest 10 | env: 11 | BASE_URL: https://api.trajano.net/ 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | with: 16 | submodules: recursive 17 | - name: 🏗 Setup Node 18 | uses: actions/setup-node@v3 19 | with: 20 | cache-dependency-path: './yarn.lock' 21 | node-version: lts/gallium 22 | cache: yarn 23 | - name: 🏗 Setup Expo 24 | uses: expo/expo-github-action@v7 25 | with: 26 | expo-version: latest 27 | eas-version: latest 28 | token: ${{ secrets.EXPO_TOKEN }} 29 | - name: 📦 Install dependencies 30 | run: yarn install --frozen-lockfile --network-concurrency 2 31 | # - name: Create env 32 | # run: | 33 | # echo BASE_URL=https://api.trajano.net/ > .env 34 | # working-directory: ./expo-app 35 | - name: 📦 Prepare 36 | run: yarn workspaces run prepare 37 | - name: 📦 Test 38 | run: yarn workspaces run test 39 | 40 | - name: 🚀 Publish app update for EAS 41 | run: eas update --non-interactive --auto 42 | working-directory: ./expo-app 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | .idea/ 7 | *.iml 8 | *.ipr 9 | *.iws 10 | .env 11 | report.json* 12 | 13 | jmeter.log 14 | node_modules 15 | *.log 16 | .DS_Store 17 | coverage/ 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "react-hooks"] 2 | path = react-hooks 3 | url = https://github.com/trajano/react-hooks 4 | [submodule "packages/react-hooks"] 5 | path = packages/react-hooks 6 | url = https://github.com/trajano/react-hooks 7 | [submodule "packages/react-navigation-header-buttons"] 8 | path = packages/react-navigation-header-buttons 9 | url = https://github.com/trajano/react-navigation-header-buttons 10 | branch = patch-1 11 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then 5 | exec >/dev/tty 2>&1 6 | else 7 | exec >/dev/console 2>&1 8 | fi 9 | npx --no lint-staged 10 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 17 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.19.0 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "settle": 1000, 3 | "ignore_dirs": [ 4 | ".git", 5 | "build" 6 | ] 7 | } -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry: https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /app.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "expo-app" 5 | }, 6 | { 7 | "path": "packages/auth-context" 8 | }, 9 | { 10 | "path": "packages/react-hooks" 11 | }, 12 | { 13 | "path": "packages/react-navigation-header-buttons" 14 | }, 15 | { 16 | "path": "." 17 | } 18 | ], 19 | "settings": { 20 | "jest.disabledWorkspaceFolders": [ 21 | ".", 22 | "packages/react-navigation-header-buttons" 23 | ], 24 | "files.exclude": { 25 | "expo-app/": true, 26 | "packages/": true, 27 | "node_modules/": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@babel/preset-env", "@babel/preset-react"], 3 | plugins: ["@babel/plugin-transform-runtime"] 4 | }; 5 | -------------------------------------------------------------------------------- /bin/extract.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | for jar in *.jar 4 | do 5 | if [ "$( jar tf $jar BOOT-INF/layers.idx )" ] 6 | then 7 | DIR=$(basename $jar -0.0.1-SNAPSHOT.jar) 8 | mkdir $DIR 9 | java -Djarmode=layertools -jar $jar extract --destination $DIR 10 | fi 11 | done 12 | -------------------------------------------------------------------------------- /bin/jmeter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -x 4 | C=$(docker run \ 5 | --memory 1g \ 6 | -e JVM_XMN=256 \ 7 | -e JVM_XMS=800 \ 8 | -e JVM_XMX=800 \ 9 | --add-host=host.docker.internal:host-gateway \ 10 | -v ${PWD}/gateway/src/test/jmeter:/work:ro \ 11 | -d \ 12 | justb4/jmeter -n -t /work/load-test.jmx \ 13 | -JbaseUri=http://host.docker.internal:28082 \ 14 | -JmaxConcurrentUsers=500 \ 15 | -JloopCount=20 \ 16 | -l /results.jtl -e -o /results) 17 | docker attach $C 18 | mkdir -p gateway/build/jmeter/results/ 19 | docker cp $C:/results.jtl gateway/build/jmeter/results.jtl 20 | docker cp $C:/results/ gateway/build/jmeter/ 21 | docker rm $C 22 | 23 | -------------------------------------------------------------------------------- /bin/loadtest-ludicrous.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -x 4 | # artillery run -q --output build/report.json --environment localhost-direct-heavy-load gateway/src/test/artillery/functional-test.yml 5 | # artillery run --output build/report.json --environment localhost-direct-heavy-load gateway/src/test/artillery/functional-test.yml 6 | # artillery run --output build/report.json --environment localhost-direct-heavy-load gateway/src/test/artillery/kill-ping.yml 7 | # artillery run --output build/report.json --environment localhost-direct-heavy-load gateway/src/test/artillery/happy-path.yml 8 | artillery run --output build/report.json --environment localhost-direct-ludicrous-load gateway/src/test/artillery/happy-path.yml 9 | artillery report build/report.json 10 | 11 | #DEBUG=http artillery run --output build/ppe.json --environment api-heavy-load gateway/src/test/artillery/happy-path.yml 12 | # 13 | #artillery report build/ppe.json 14 | -------------------------------------------------------------------------------- /bin/loadtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -x 4 | # artillery run -q --output build/report.json --environment localhost-direct-heavy-load gateway/src/test/artillery/functional-test.yml 5 | # artillery run --output build/report.json --environment localhost-direct-heavy-load gateway/src/test/artillery/functional-test.yml 6 | # artillery run --output build/report.json --environment localhost-direct-heavy-load gateway/src/test/artillery/kill-ping.yml 7 | # artillery run --output build/report.json --environment localhost-direct-heavy-load gateway/src/test/artillery/happy-path.yml 8 | artillery run --output build/report.json --environment localhost-direct-heavy-load gateway/src/test/artillery/happy-path-no-grpc.yml 9 | artillery report build/report.json 10 | 11 | #DEBUG=http artillery run --output build/ppe.json --environment api-heavy-load gateway/src/test/artillery/happy-path.yml 12 | # 13 | #artillery report build/ppe.json 14 | -------------------------------------------------------------------------------- /build-prometheus.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | docker compose build prometheus 4 | # ( echo -e "version: '3.9'\n"; docker-compose config )| docker stack deploy -c - --with-registry-auth --prune ds 5 | #docker service update -d --image local/jwks-provider --force ds_jwks-provider 6 | #docker service update --image local/gateway --force ds_gateway 7 | docker service update --image docker.local/prometheus --force ds_prometheus 8 | -------------------------------------------------------------------------------- /build-service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | ./gradlew spotlessApply 4 | docker compose build sample grpc-sample 5 | # ( echo -e "version: '3.9'\n"; docker-compose config )| docker stack deploy -c - --with-registry-auth --prune ds 6 | #docker service update -d --image local/jwks-provider --force ds_jwks-provider 7 | #docker service update --image local/gateway --force ds_gateway 8 | docker service update --image docker.local/sample-service --force ds_sample 9 | docker service update --image docker.local/grpc-service --force ds_grpc-sample 10 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'idea' 3 | id "com.diffplug.spotless" version "6.16.0" 4 | id "com.github.ben-manes.versions" version "0.46.0" 5 | id "org.sonarqube" version "4.0.0.2929" 6 | } 7 | spotless { 8 | format "misc", { 9 | target '*.gradle', '*.md', '.gitignore' 10 | trimTrailingWhitespace() 11 | endWithNewline() 12 | } 13 | } 14 | repositories { 15 | mavenCentral() 16 | } 17 | gradleEnterprise { 18 | if (System.getenv("CI") != null) { 19 | buildScan { 20 | publishAlways() 21 | termsOfServiceUrl = "https://gradle.com/terms-of-service" 22 | termsOfServiceAgree = "yes" 23 | } 24 | } 25 | } 26 | 27 | sonarqube { 28 | properties { 29 | property "sonar.projectKey", "trajano_spring-cloud-demo" 30 | property "sonar.organization", "trajano" 31 | property "sonar.host.url", "https://sonarcloud.io" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | docker compose pull --ignore-buildable & 4 | ./gradlew spotlessApply 5 | docker compose build 6 | wait 7 | docker stack deploy -c docker-compose.yml --with-registry-auth --prune ds 8 | # docker service update -d --image local/jwks-provider --force ds_jwks-provider 9 | # docker service update -d --image local/grpc-service --force ds_grpc-sample 10 | docker service update --image docker.local/gateway --force ds_gateway 11 | -------------------------------------------------------------------------------- /buildSrc/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'groovy-gradle-plugin' 3 | } 4 | 5 | repositories { 6 | gradlePluginPortal() 7 | } 8 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | docker stack deploy -c docker-compose-deploy.yml --prune ds 2 | -------------------------------------------------------------------------------- /dynamic-grpc-client/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /dynamic-grpc-client/README.md: -------------------------------------------------------------------------------- 1 | # Dynamic GRPC Client 2 | This encapsulates a GRPC client that is dynamic. 3 | 4 | This does this by using the reflection API from GRPC. 5 | 6 | It should ideally have a similar API as WebClient API 7 | 8 | ```java 9 | WebClient client = WebClient.builder() 10 | .baseUrl("http://localhost:8080") 11 | .defaultCookie("cookieKey", "cookieValue") 12 | .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 13 | .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080")) 14 | .build(); 15 | ``` 16 | 17 | ```java 18 | DynamicGrpcClient client = DynamicGrpcClient.builder() 19 | .endpoint("http://localhost:50000") 20 | .build(); 21 | ``` 22 | 23 | ```java 24 | 25 | // given a `Channel` and body 26 | // obtain the reflection stuff 27 | // translate JSON to the message 28 | // send and receive 29 | // translate message to JSON 30 | 31 | ``` 32 | 33 | This component will have no notion of Spring Cloud Gateway 34 | -------------------------------------------------------------------------------- /dynamic-grpc-client/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'io.spring.dependency-management' version '1.1.0' 3 | id 'net.trajano.swarm.conventions' 4 | id 'java-library' 5 | } 6 | 7 | group = 'net.trajano.swarm' 8 | version = '0.0.1-SNAPSHOT' 9 | 10 | configurations { 11 | compileOnly { 12 | extendsFrom annotationProcessor 13 | } 14 | } 15 | 16 | jar { 17 | enabled = true 18 | } 19 | dependencies { 20 | 21 | api "io.grpc:grpc-api" 22 | api "io.grpc:grpc-protobuf" 23 | api "io.grpc:grpc-services" 24 | 25 | api 'org.springframework:spring-webflux' 26 | compileOnly 'org.projectlombok:lombok' 27 | annotationProcessor 'org.projectlombok:lombok' 28 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 29 | } 30 | 31 | dependencyManagement { 32 | imports { 33 | mavenBom "io.grpc:grpc-bom:${grpcVersion}" 34 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 35 | mavenBom "org.springframework.boot:spring-boot-dependencies:${springBootVersion}" 36 | } 37 | } 38 | 39 | tasks.named('test') { 40 | useJUnitPlatform() 41 | } 42 | -------------------------------------------------------------------------------- /dynamic-grpc-client/src/main/java/net/trajano/swarm/grpc/GrpcMessageContext.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.grpc; 2 | 3 | import io.grpc.Channel; 4 | import io.grpc.MethodDescriptor; 5 | import io.grpc.reflection.v1alpha.ServerReflectionGrpc; 6 | import java.net.URI; 7 | import java.util.List; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Data; 11 | import lombok.With; 12 | import org.springframework.web.server.ServerWebExchange; 13 | 14 | @Data 15 | @With 16 | @Builder 17 | @AllArgsConstructor 18 | public class GrpcMessageContext { 19 | /** GRPC Channel. This must be present. */ 20 | private final Channel channel; 21 | 22 | private final String serviceName; 23 | private final String methodName; 24 | private ServerWebExchange serverWebExchange; 25 | private URI requestUri; 26 | /** Request JSON. */ 27 | private String requestJson; 28 | 29 | private MethodDescriptor methodDescriptor; 30 | 31 | public ServerReflectionGrpc.ServerReflectionStub getServerReflectionStub() { 32 | return ServerReflectionGrpc.newStub(channel); 33 | } 34 | 35 | private List services; 36 | } 37 | -------------------------------------------------------------------------------- /dynamic-grpc-client/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /expo-app/.expo-shared/README.md: -------------------------------------------------------------------------------- 1 | > Why do I have a folder named ".expo-shared" in my project? 2 | 3 | The ".expo-shared" folder is created when running commands that produce state that is intended to be shared with all developers on the project. For example, "npx expo-optimize". 4 | 5 | > What does the "assets.json" file contain? 6 | 7 | The "assets.json" file describes the assets that have been optimized through "expo-optimize" and do not need to be processed again. 8 | 9 | > Should I commit the ".expo-shared" folder? 10 | 11 | Yes, you should share the ".expo-shared" folder with your collaborators. 12 | -------------------------------------------------------------------------------- /expo-app/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /expo-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /expo-app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jest.jestCommandLine": "yarn test", 3 | "sonarlint.connectedMode.project": { 4 | "connectionId": "trajano", 5 | "projectKey": "spring-docker-swarm-expo-app" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /expo-app/app.config.ts: -------------------------------------------------------------------------------- 1 | import type { ExpoConfig } from "@expo/config"; 2 | export default ({ config }: { config: ExpoConfig }): ExpoConfig => ({ 3 | ...config, 4 | name: process.env.APP_NAME ?? config.name, 5 | icon: process.env.APP_ICON ?? config.icon, 6 | // jsEngine: process.env.JS_ENGINE as ExpoConfig["jsEngine"], 7 | ios: { 8 | ...config.ios, 9 | bundleIdentifier: process.env.BUNDLE_ID ?? config.ios?.bundleIdentifier, 10 | }, 11 | android: { 12 | ...config.android, 13 | package: process.env.BUNDLE_ID ?? config.android?.package, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /expo-app/assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/expo-app/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /expo-app/assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/expo-app/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /expo-app/assets/images/dev-client-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/expo-app/assets/images/dev-client-icon.png -------------------------------------------------------------------------------- /expo-app/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/expo-app/assets/images/favicon.png -------------------------------------------------------------------------------- /expo-app/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/expo-app/assets/images/icon.png -------------------------------------------------------------------------------- /expo-app/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/expo-app/assets/images/splash.png -------------------------------------------------------------------------------- /expo-app/authenticated-context/IAuthenticated.ts: -------------------------------------------------------------------------------- 1 | import * as SQLite from "expo-sqlite"; 2 | 3 | import { JwtClaims } from "./JwtClaims"; 4 | 5 | export interface IAuthenticated { 6 | /** 7 | * Some state goes here that's populated from the event stream. This may be a 8 | * type parameter later. 9 | */ 10 | internalState: string[]; 11 | username: string; 12 | verified: boolean; 13 | claims?: JwtClaims; 14 | dbLoaded: boolean; 15 | db?: SQLite.Database; 16 | /** This invokes the whoami endpoint */ 17 | whoami: () => Promise>; 18 | } 19 | -------------------------------------------------------------------------------- /expo-app/authenticated-context/IAuthenticatedContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | import { IAuthenticated } from "./IAuthenticated"; 4 | export const AuthenticatedContext = createContext({ 5 | internalState: [], 6 | username: "", 7 | verified: false, 8 | dbLoaded: false, 9 | whoami: () => Promise.resolve({}), 10 | }); 11 | -------------------------------------------------------------------------------- /expo-app/authenticated-context/JwtClaims.ts: -------------------------------------------------------------------------------- 1 | export interface JwtClaims { 2 | [key: string]: unknown; 3 | sub: string; 4 | aud: string[]; 5 | exp: number; 6 | iss: string; 7 | } 8 | -------------------------------------------------------------------------------- /expo-app/authenticated-context/actions.ts: -------------------------------------------------------------------------------- 1 | export const AUTHENTICATED = "AUTHENTICATED"; 2 | export const SSE_EVENT = "SSE_EVENT"; 3 | -------------------------------------------------------------------------------- /expo-app/authenticated-context/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This provides the services to the front end using the authentication that is 3 | * provided. 4 | */ 5 | export { AuthenticatedProvider } from "./AuthenticatedProvider"; 6 | export { useAuthenticated } from "./useAuthenticated"; 7 | -------------------------------------------------------------------------------- /expo-app/authenticated-context/jwtVerify.web.ts: -------------------------------------------------------------------------------- 1 | import base64url from "base64url"; 2 | import * as jose from "jose"; 3 | import * as pako from "pako"; 4 | 5 | import { JwtClaims } from "./JwtClaims"; 6 | 7 | /** 8 | * @param accessToken Access token (may be a compressed JWT) 9 | * @param jwksUrl 10 | * @param clientId 11 | * @returns The payload converted to an object 12 | */ 13 | export async function jwtVerify

( 14 | accessToken: string | null, 15 | jwksUrl: URL, 16 | issuer: string, 17 | clientId: string 18 | ): Promise

{ 19 | if (accessToken === null) { 20 | return { 21 | sub: "", 22 | iss: "", 23 | aud: [], 24 | exp: 0, 25 | } as unknown as P; 26 | } 27 | const decodedCompressed = base64url.toBuffer(accessToken); 28 | const jwt = pako.inflate(decodedCompressed, { to: "string" }); 29 | const jwks = jose.createRemoteJWKSet(jwksUrl); 30 | const { payload } = await jose.jwtVerify(jwt, jwks, { 31 | audience: clientId, 32 | issuer, 33 | }); 34 | return payload as P; 35 | } 36 | -------------------------------------------------------------------------------- /expo-app/authenticated-context/reducers.ts: -------------------------------------------------------------------------------- 1 | import Constants from "expo-constants"; 2 | export const name = Constants.expoConfig?.name; 3 | 4 | /* 5 | * import type { PayloadAction } from "@reduxjs/toolkit"; 6 | * import { AUTHENTICATED, SSE_EVENT } from "./actions"; 7 | * import { JwtClaims } from "./JwtClaims"; 8 | * export const claims = (state = {}, action: PayloadAction) => { 9 | * if (action.type === AUTHENTICATED) { 10 | * return { ...state, claims: action.payload }; 11 | * } 12 | * return state; 13 | * }; 14 | * export const sseState = (state = [], action: PayloadAction) => { 15 | * if (action.type === SSE_EVENT) { 16 | * return [...state, action.payload].slice(-5); 17 | * } 18 | * return state; 19 | * }; 20 | */ 21 | -------------------------------------------------------------------------------- /expo-app/authenticated-context/sseEventSlice.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | /* 3 | * import { createSlice } from "@reduxjs/toolkit"; 4 | * import type { PayloadAction } from "@reduxjs/toolkit"; 5 | */ 6 | 7 | /* 8 | * const initialState: string[] = []; 9 | * export const sseEventSlice = createSlice({ 10 | * name: "sseEvent", 11 | * initialState, 12 | * reducers: { 13 | * receivedEvent(state, action: PayloadAction) { 14 | * state.push(action.payload); 15 | * state.splice(0, Math.max(state.length - 5, 0)); 16 | * }, 17 | * }, 18 | * }); 19 | * export const { receivedEvent } = sseEventSlice.actions; 20 | */ 21 | -------------------------------------------------------------------------------- /expo-app/authenticated-context/useAuthenticated.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | 3 | import { IAuthenticated } from "./IAuthenticated"; 4 | import { AuthenticatedContext } from "./IAuthenticatedContext"; 5 | 6 | export function useAuthenticated(): IAuthenticated { 7 | return useContext(AuthenticatedContext); 8 | } 9 | -------------------------------------------------------------------------------- /expo-app/authenticated-context/useDb.ts: -------------------------------------------------------------------------------- 1 | import * as SQLite from "expo-sqlite"; 2 | import { useEffect, useRef } from "react"; 3 | interface DbLoadedState { 4 | loaded: true; 5 | db: SQLite.Database; 6 | } 7 | interface DbUnloadedState { 8 | loaded: false; 9 | db: undefined; 10 | } 11 | type DbState = DbLoadedState | DbUnloadedState; 12 | export function useDb(databaseName: string): DbState { 13 | const dbRef = useRef(); 14 | useEffect(() => { 15 | SQLite.openDatabase( 16 | databaseName, 17 | undefined, 18 | undefined, 19 | undefined, 20 | (nextDb) => { 21 | nextDb.exec( 22 | [{ sql: "PRAGMA foreign_keys = ON;", args: [] }], 23 | false, 24 | () => { 25 | dbRef.current = nextDb; 26 | } 27 | ); 28 | } 29 | ); 30 | return () => { 31 | if (dbRef.current !== undefined) { 32 | dbRef.current.closeAsync(); 33 | dbRef.current = undefined; 34 | } 35 | }; 36 | }); 37 | if (dbRef.current) { 38 | return { 39 | loaded: true, 40 | db: dbRef.current, 41 | }; 42 | } else { 43 | return { 44 | loaded: false, 45 | db: undefined, 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /expo-app/authenticated-context/useDb.web.ts: -------------------------------------------------------------------------------- 1 | interface DbUnloadedState { 2 | loaded: false; 3 | db: undefined; 4 | } 5 | type DbState = DbUnloadedState; 6 | /** 7 | * WebSQL lite is not supported on the web specifically on firefox 8 | * 9 | * @param databaseName 10 | * @returns 11 | */ 12 | export function useDb(_databaseName: string): DbState { 13 | return { 14 | loaded: false, 15 | db: undefined, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /expo-app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: [ 6 | [ 7 | "module:react-native-dotenv", 8 | { 9 | allowUndefined: true, 10 | }, 11 | ], 12 | "@babel/plugin-proposal-export-namespace-from", 13 | "react-native-reanimated/plugin", 14 | ], 15 | env: { 16 | production: { 17 | // plugins: ["react-native-paper/babel"], 18 | }, 19 | }, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /expo-app/constants/Colors.ts: -------------------------------------------------------------------------------- 1 | const tintColorLight = "#2f95dc"; 2 | const tintColorDark = "#fff"; 3 | 4 | export default { 5 | light: { 6 | text: "#000", 7 | background: "#fff", 8 | tint: tintColorLight, 9 | tabIconDefault: "#ccc", 10 | tabIconSelected: tintColorLight, 11 | }, 12 | dark: { 13 | text: "#fff", 14 | background: "#000", 15 | tint: tintColorDark, 16 | tabIconDefault: "#ccc", 17 | tabIconSelected: tintColorDark, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /expo-app/constants/Layout.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from "react-native"; 2 | 3 | const width = Dimensions.get("window").width; 4 | const height = Dimensions.get("window").height; 5 | 6 | export default { 7 | window: { 8 | width, 9 | height, 10 | }, 11 | isSmallDevice: width < 375, 12 | }; 13 | -------------------------------------------------------------------------------- /expo-app/eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "development": { 4 | "developmentClient": true, 5 | "distribution": "internal", 6 | "channel": "rework", 7 | "env": { 8 | "APP_NAME": "Spring Dev Client", 9 | "APP_ICON": "./assets/images/dev-client-icon.png", 10 | "BUNDLE_ID": "com.trajano.expoapp.dev" 11 | } 12 | }, 13 | "development-simulator": { 14 | "developmentClient": true, 15 | "distribution": "internal", 16 | "channel": "rework", 17 | "ios": { 18 | "simulator": true 19 | }, 20 | "env": { 21 | "APP_NAME": "Spring Dev Client", 22 | "APP_ICON": "./assets/images/dev-client-icon.png", 23 | "BUNDLE_ID": "com.trajano.expoapp.dev" 24 | } 25 | }, 26 | "preview": { 27 | "distribution": "internal", 28 | "channel": "rework", 29 | "env": { 30 | "JS_ENGINE": "hermes" 31 | } 32 | }, 33 | "production": { 34 | "channel": "rework", 35 | "env": { 36 | "JS_ENGINE": "hermes" 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /expo-app/eas.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm install -g eas-cli 3 | eas build --profile development --platform all --no-wait --non-interactive --clear-cache 4 | eas build --profile development-simulator --platform ios --no-wait --non-interactive --clear-cache 5 | eas build --profile preview --platform all --no-wait --non-interactive --clear-cache 6 | eas update --non-interactive --auto 7 | -------------------------------------------------------------------------------- /expo-app/easupdate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | npm install -g eas-cli 4 | yarn tsc 5 | eas update --non-interactive --auto 6 | -------------------------------------------------------------------------------- /expo-app/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@env" { 2 | export const BASE_URL: string?; 3 | export const TEXT_TEST: boolean?; 4 | } 5 | -------------------------------------------------------------------------------- /expo-app/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require("expo/metro-config"); 2 | const findWorkspaceRoot = require("find-yarn-workspace-root"); 3 | const path = require("path"); 4 | 5 | const projectRoot = __dirname; 6 | const workspaceRoot = findWorkspaceRoot(__dirname); 7 | 8 | const config = getDefaultConfig(projectRoot); 9 | 10 | // 1. Watch all files within the monorepo 11 | config.watchFolders = [workspaceRoot]; 12 | // 2. Let Metro know where to resolve packages and in what order 13 | config.resolver.nodeModulesPaths = [ 14 | path.resolve(projectRoot, "node_modules"), 15 | path.resolve(workspaceRoot, "node_modules"), 16 | ]; 17 | // 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths` 18 | config.resolver.disableHierarchicalLookup = true; 19 | 20 | config.resolver.assetExts.push("json"); 21 | 22 | let currentBlockList = config.resolver.blockList; 23 | if (!Array.isArray(currentBlockList)) { 24 | currentBlockList = [currentBlockList]; 25 | } 26 | config.resolver.blockList = [...currentBlockList, /\\.git/]; 27 | console.log(config) 28 | module.exports = config; 29 | -------------------------------------------------------------------------------- /expo-app/navigation/LinkingConfiguration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about deep linking with React Navigation 3 | * https://reactnavigation.org/docs/deep-linking 4 | * https://reactnavigation.org/docs/configuring-links 5 | */ 6 | 7 | import { LinkingOptions } from "@react-navigation/native"; 8 | import * as Linking from "expo-linking"; 9 | 10 | import { RootStackParamList } from "./paramLists"; 11 | 12 | const linking: LinkingOptions = { 13 | prefixes: [Linking.createURL("/")], 14 | config: { 15 | screens: { 16 | Root: { 17 | screens: { 18 | MainDrawer: { 19 | screens: { 20 | TabOne: { 21 | screens: { 22 | TabOneScreen: "one", 23 | }, 24 | }, 25 | }, 26 | }, 27 | TabTwo: "two", 28 | NetworkLogger: "network-log", 29 | }, 30 | }, 31 | Modal: "modal", 32 | NotFound: "*", 33 | }, 34 | }, 35 | }; 36 | 37 | export default linking; 38 | -------------------------------------------------------------------------------- /expo-app/navigation/login/LoginNavigator.tsx: -------------------------------------------------------------------------------- 1 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 2 | 3 | import LoginScreen from "./LoginScreen"; 4 | import { LoginStackParamList } from "./types"; 5 | import { View } from "../../src/lib/native-unstyled"; 6 | 7 | const Stack = createNativeStackNavigator(); 8 | export function LoginNavigator() { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /expo-app/navigation/login/types.ts: -------------------------------------------------------------------------------- 1 | import { ParamListBase } from "@react-navigation/native"; 2 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 3 | import { EndpointConfiguration } from "@trajano/spring-docker-auth-context"; 4 | 5 | export interface LoginStackParamList extends ParamListBase { 6 | Login: undefined; 7 | Modal: undefined; 8 | } 9 | export type LoginStackScreenProps = 10 | NativeStackScreenProps; 11 | export type AuthenticatedEndpointConfiguration = EndpointConfiguration & { 12 | /** Whoami endpoint. Defaults to `whoami/` */ 13 | whoamiEndpoint?: string; 14 | verifyClaims?: boolean; 15 | }; 16 | -------------------------------------------------------------------------------- /expo-app/screens/ModalScreen.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from "expo-status-bar"; 2 | import { Platform, StyleSheet } from "react-native"; 3 | 4 | import { Text, View } from "../src/lib/native-unstyled"; 5 | 6 | export default function ModalScreen() { 7 | return ( 8 | 9 | Modal 10 | 11 | 12 | {/* Use a light status bar on iOS to account for the black space above the modal */} 13 | 14 | 15 | ); 16 | } 17 | 18 | const styles = StyleSheet.create({ 19 | container: { 20 | flex: 1, 21 | alignItems: "center", 22 | justifyContent: "center", 23 | }, 24 | title: { 25 | fontSize: 20, 26 | fontWeight: "bold", 27 | }, 28 | separator: { 29 | marginVertical: 30, 30 | height: 1, 31 | width: "80%", 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /expo-app/screens/NetworkLoggerScreen.tsx: -------------------------------------------------------------------------------- 1 | import NetInfo, { 2 | NetInfoState, 3 | NetInfoStateType, 4 | } from "@react-native-community/netinfo"; 5 | import { useFocusEffect } from "@react-navigation/native"; 6 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 7 | import { useCallback } from "react"; 8 | import { AppState } from "react-native"; 9 | import NetworkLogger from "react-native-network-logger"; 10 | 11 | export function NetworkLoggerScreen({ 12 | navigation, 13 | }: NativeStackScreenProps) { 14 | const networkInfoHandler = useCallback( 15 | (nextState: NetInfoState) => { 16 | navigation.setOptions({ 17 | title: `${NetInfoStateType[nextState.type]} c=${ 18 | nextState.isConnected ? "Y" : "N" 19 | } i=${nextState.isInternetReachable ? "Y" : "N"} s=${ 20 | AppState.currentState 21 | }`, 22 | }); 23 | }, 24 | [navigation] 25 | ); 26 | useFocusEffect( 27 | useCallback(() => { 28 | const cancelNetInfoSubscription = 29 | NetInfo.addEventListener(networkInfoHandler); 30 | return () => cancelNetInfoSubscription(); 31 | }, [networkInfoHandler]) 32 | ); 33 | return ( 34 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /expo-app/screens/NetworkLoggerScreen.web.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "react-native"; 2 | 3 | export function NetworkLoggerScreen() { 4 | return Not supported; 5 | } 6 | -------------------------------------------------------------------------------- /expo-app/screens/NetworkLoggerTab.tsx: -------------------------------------------------------------------------------- 1 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 2 | 3 | import { NetworkLoggerScreen } from "./NetworkLoggerScreen"; 4 | const Stack = createNativeStackNavigator(); 5 | export function NetworkLoggerTab() { 6 | return ( 7 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /expo-app/screens/NotFoundScreen.tsx: -------------------------------------------------------------------------------- 1 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 2 | import { useCallback } from "react"; 3 | import { StyleSheet, TouchableOpacity } from "react-native"; 4 | 5 | import { RootStackParamList } from "../navigation/paramLists"; 6 | import { Text, View } from "../src/lib/native-unstyled"; 7 | 8 | export default function NotFoundScreen({ 9 | navigation, 10 | }: NativeStackScreenProps) { 11 | const navigateBackToRoot = useCallback( 12 | () => navigation.replace("Root"), 13 | [navigation] 14 | ); 15 | return ( 16 | 17 | This screen doesn't exist. 18 | 19 | Go to home screen! 20 | 21 | 22 | ); 23 | } 24 | 25 | const styles = StyleSheet.create({ 26 | container: { 27 | flex: 1, 28 | alignItems: "center", 29 | justifyContent: "center", 30 | padding: 20, 31 | }, 32 | title: { 33 | fontSize: 20, 34 | fontWeight: "bold", 35 | }, 36 | link: { 37 | marginTop: 15, 38 | paddingVertical: 15, 39 | }, 40 | linkText: { 41 | fontSize: 14, 42 | color: "#2e78b7", 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /expo-app/screens/StackNavigatorScrollView/StackNavigatorScrollView.tsx: -------------------------------------------------------------------------------- 1 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 2 | 3 | import { StackNavigatorScrollViewScreen } from "./StackNavigatorScrollViewScreen"; 4 | const Stack = createNativeStackNavigator(); 5 | 6 | export function StackNavigatorScrollView() { 7 | return ( 8 | 15 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /expo-app/screens/StackNavigatorScrollView/index.ts: -------------------------------------------------------------------------------- 1 | export { StackNavigatorScrollView } from "./StackNavigatorScrollView"; 2 | -------------------------------------------------------------------------------- /expo-app/screens/SystemFontScreen.test.tsx: -------------------------------------------------------------------------------- 1 | import { hasNoVariantSuffix } from "./SystemFontsScreen"; 2 | it("FilterVariants", () => { 3 | const fonts = [ 4 | "HelveticaNeue", 5 | "HelveticaNeue-CondensedBold", 6 | "HelveticaNeue-UltraLight", 7 | "HelveticaNeue-UltraLightItalic", 8 | ]; 9 | expect(fonts.filter(hasNoVariantSuffix)).toStrictEqual(["HelveticaNeue"]); 10 | }); 11 | -------------------------------------------------------------------------------- /expo-app/screens/TextTab.tsx: -------------------------------------------------------------------------------- 1 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 2 | 3 | import TabTwoScreen from "./TabTwoScreen"; 4 | const Stack = createNativeStackNavigator(); 5 | export function TextTab() { 6 | return ( 7 | 12 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /expo-app/src/__tests__/env.test.ts: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from "@env"; 2 | test("validate typing", () => { 3 | const baseUrlUndefined = BASE_URL === undefined; 4 | const baseUrlDefined = BASE_URL !== undefined; 5 | expect(baseUrlUndefined || baseUrlDefined).toBeTruthy(); 6 | }); 7 | -------------------------------------------------------------------------------- /expo-app/src/__tests__/jsxsanity.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react-native"; 2 | import { View, ViewProps } from "react-native"; 3 | /** Tests for understanding of standard library with JSX */ 4 | it("should remove undefined", () => { 5 | const prop: ViewProps = { accessibilityLabel: "foo" }; 6 | render( 7 | 8 | 9 | 10 | 11 | ); 12 | expect(screen.getByTestId("f").props).toStrictEqual({ 13 | testID: "f", 14 | accessibilityLabel: "foo", 15 | children: undefined, 16 | }); 17 | expect(screen.getByTestId("g").props).toStrictEqual({ 18 | testID: "g", 19 | children: undefined, 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /expo-app/src/__tests__/sanity.test.ts: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from "@env"; 2 | import isEmpty from "lodash/isEmpty"; 3 | import pickBy from "lodash/pickBy"; 4 | /** Tests for understanding of standard library */ 5 | it("should remove undefined", () => { 6 | const prop = { _a: "foo" }; 7 | const prop2 = { _a: undefined, accessibilityLabel: "bar" }; 8 | const combined = { ...prop, ...prop2 }; 9 | expect(combined).toStrictEqual({ _a: undefined, accessibilityLabel: "bar" }); 10 | const combinedPicked = pickBy(combined); 11 | expect(combinedPicked).toStrictEqual({ accessibilityLabel: "bar" }); 12 | }); 13 | 14 | it("should not crash importing BASE_URL whether it has a value or not", () => { 15 | expect(BASE_URL ?? "a").toBeTruthy(); 16 | }); 17 | 18 | it("isEmpty", () => { 19 | expect(isEmpty(undefined)).toBeTruthy(); 20 | expect(isEmpty("")).toBeTruthy(); 21 | expect(isEmpty([])).toBeTruthy(); 22 | expect(isEmpty({})).toBeTruthy(); 23 | }); 24 | -------------------------------------------------------------------------------- /expo-app/src/__tests__/useColorScheme.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from "@testing-library/react-native"; 2 | import { useColorScheme } from "react-native"; 3 | /** Tests for understanding of standard library */ 4 | it("should remove undefined", () => { 5 | const { result } = renderHook(() => useColorScheme(), {}); 6 | expect(result.current).toBe("light"); 7 | }); 8 | -------------------------------------------------------------------------------- /expo-app/src/app-context/AppContext.ts: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | import { createContext } from "react"; 3 | 4 | import type { IAppContext } from "./IAppContext"; 5 | import defaultAppContext from "./default"; 6 | 7 | /** 8 | * The Xyz context. It is initially set with reasonable defaults to avoid the 9 | * need for null checks. 10 | */ 11 | export const AppContext = createContext(defaultAppContext); 12 | -------------------------------------------------------------------------------- /expo-app/src/app-context/AppProviderProps.ts: -------------------------------------------------------------------------------- 1 | /** @module */ 2 | import type { PropsWithChildren } from "react"; 3 | 4 | import { AppEvent } from "../lib/app-log/AppEvent"; 5 | /** Provider initialization props */ 6 | export type AppProviderProps = PropsWithChildren<{ 7 | /** 8 | * Predicate to determine whether to log the event. Defaults to accept all 9 | * except `Connection` and `CheckRefresh` which are polling events. 10 | */ 11 | logAuthEventFilterPredicate?: (event: AppEvent) => boolean; 12 | /** Size of the auth event log. Defaults to 50 */ 13 | logAuthEventSize?: number; 14 | }>; 15 | -------------------------------------------------------------------------------- /expo-app/src/app-context/IAppContext.ts: -------------------------------------------------------------------------------- 1 | import { LoggedAuthEvent } from "../lib/app-log/LoggedAuthEvent"; 2 | 3 | /** 4 | * Interface that the context would provide. `interface` instead of `type`, this 5 | * is because a context value is not just a set of values but also functions, so 6 | * `interface` is a more natural use of it. When this exported it drops the `I` 7 | * prefix. 8 | * 9 | * Even if this can be thought of as an internal representation, exporting it 10 | * allows TypeDoc to render the documentation. 11 | * 12 | * Another approach could be `Xyz` but there's a good likelihood that an 13 | * existing third-party module will be exposing the same type, as such suffixing 14 | * it with `Context` mitigates the issue. 15 | * 16 | * @internal 17 | */ 18 | export interface IAppContext { 19 | /** 20 | * Last auth events. The most recent one will be the first element. This is 21 | * primarily used to diagnose issues where the token becomes invalidated and 22 | * the user was forcefully logged out. 23 | */ 24 | lastAuthEvents: LoggedAuthEvent[]; 25 | clearLastAuthEvents: () => void; 26 | } 27 | -------------------------------------------------------------------------------- /expo-app/src/app-context/default.ts: -------------------------------------------------------------------------------- 1 | import noop from "lodash/noop"; 2 | 3 | import { IAppContext } from "./IAppContext"; 4 | 5 | /** 6 | * Default context implementation that would provide stubs or null object 7 | * values. 8 | */ 9 | export default { 10 | lastAuthEvents: [], 11 | clearLastAuthEvents: noop, 12 | } as IAppContext; 13 | -------------------------------------------------------------------------------- /expo-app/src/app-context/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This provides a boiler plate code for a React context. 3 | * 4 | * @module 5 | */ 6 | export * from "./AppProvider"; 7 | export type { AppProviderProps } from "./AppProviderProps"; 8 | export type { IAppContext as AppContext } from "./IAppContext"; 9 | export * from "./useApp"; 10 | -------------------------------------------------------------------------------- /expo-app/src/app-context/useApp.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | 3 | import { AppContext } from "./AppContext"; 4 | import type { IAppContext } from "./IAppContext"; 5 | 6 | /** 7 | * Provides the interface to work with the Xyz context. 8 | * 9 | * @returns Interface to work with Xyz context 10 | */ 11 | export function useApp(): IAppContext { 12 | return useContext(AppContext); 13 | } 14 | -------------------------------------------------------------------------------- /expo-app/src/app-context/useBackendReachable.ts: -------------------------------------------------------------------------------- 1 | import NetInfo from "@react-native-community/netinfo"; 2 | import type { EndpointConfiguration } from "@trajano/spring-docker-auth-context"; 3 | import { useEffect, useState } from "react"; 4 | 5 | export const useBackendReachable = ( 6 | endpointConfiguration: EndpointConfiguration 7 | ): boolean => { 8 | const [backendReachable, setBackendReachable] = useState(false); 9 | useEffect(() => { 10 | NetInfo.configure({ 11 | reachabilityUrl: endpointConfiguration.pingEndpoint, 12 | reachabilityTest: (response) => 13 | Promise.resolve(response.status === 200 || response.status === 204), 14 | useNativeReachability: true, 15 | }); 16 | 17 | (async () => { 18 | const nextStatus = await NetInfo.refresh(); 19 | setBackendReachable(nextStatus.isInternetReachable === true); 20 | })(); 21 | 22 | return NetInfo.addEventListener((nextStatus) => { 23 | setBackendReachable(nextStatus.isInternetReachable === true); 24 | }); 25 | }, [endpointConfiguration.pingEndpoint]); 26 | return backendReachable; 27 | }; 28 | -------------------------------------------------------------------------------- /expo-app/src/app-context/useLastAuthEvents.ts: -------------------------------------------------------------------------------- 1 | import { useReducer } from "react"; 2 | 3 | import { AppEvent } from "../lib/app-log/AppEvent"; 4 | import { LoggedAuthEvent } from "../lib/app-log/LoggedAuthEvent"; 5 | 6 | export function useLastAuthEvents( 7 | logAuthEventFilterPredicate: (event: AppEvent) => boolean, 8 | logAuthEventSize: number 9 | ) { 10 | return useReducer( 11 | ( 12 | current: LoggedAuthEvent[], 13 | nextAuthEvent: AppEvent 14 | ): LoggedAuthEvent[] => { 15 | if (nextAuthEvent.type === "App" && nextAuthEvent.reason === "CLEAR") { 16 | return []; 17 | } else if (logAuthEventFilterPredicate(nextAuthEvent)) { 18 | return [ 19 | { 20 | ...nextAuthEvent, 21 | key: `${nextAuthEvent.type}.${Date.now()}`, 22 | reason: `${nextAuthEvent.type} ${nextAuthEvent.reason ?? ""}`, 23 | on: new Date(), 24 | } as LoggedAuthEvent, 25 | ...current, 26 | ].slice(0, logAuthEventSize); 27 | } else { 28 | return current; 29 | } 30 | }, 31 | [] 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /expo-app/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { PressableProps } from "react-native"; 2 | 3 | import { 4 | Pressable, 5 | StyleProps, 6 | Text, 7 | useTheming, 8 | } from "../lib/native-unstyled"; 9 | 10 | type ButtonProps = StyleProps & 11 | PressableProps & { 12 | children: string | string[]; 13 | }; 14 | 15 | export const Button = ({ children, onPress }: ButtonProps) => { 16 | const { colors } = useTheming(); 17 | 18 | return ( 19 | 25 | {children} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /expo-app/src/components/ErrorView/ErrorView.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | 3 | import { SimpleErrorView } from "./SimpleErrorView"; 4 | import { StackTraceErrorView } from "./StackTraceErrorView"; 5 | import type { StyleProps } from "../../lib/native-unstyled"; 6 | type ErrorViewProps = StyleProps & { 7 | exception: unknown; 8 | }; 9 | 10 | /** This is a error view that will take up the full area that is given to it. */ 11 | export const ErrorView = memo(({ exception }: ErrorViewProps) => { 12 | if (!exception) { 13 | return ; 14 | } else if (typeof exception === "object") { 15 | if (exception instanceof Error) { 16 | return ; 17 | } else { 18 | return ; 19 | } 20 | } else { 21 | return ; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /expo-app/src/components/ErrorView/SimpleErrorView.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | 3 | import { View } from "../../lib/native-unstyled"; 4 | export function SimpleErrorView({ 5 | message, 6 | }: { 7 | message: string; 8 | }): ReactElement { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /expo-app/src/components/ErrorView/StackTraceErrorView.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | 3 | import { View } from "../../lib/native-unstyled"; 4 | export function StackTraceErrorView({ 5 | exception, 6 | }: { 7 | exception: Error; 8 | }): ReactElement { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /expo-app/src/components/ErrorView/index.ts: -------------------------------------------------------------------------------- 1 | export { ErrorView } from "./ErrorView"; 2 | -------------------------------------------------------------------------------- /expo-app/src/components/Text.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/expo-app/src/components/Text.tsx -------------------------------------------------------------------------------- /expo-app/src/header-test/HeaderTestNavigationContainer.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationContainer } from "@react-navigation/native"; 2 | import * as SplashScreen from "expo-splash-screen"; 3 | import { useEffect } from "react"; 4 | 5 | import { MainTabNavigation, MainTabParamList } from "./MainTabNavigation"; 6 | import { useTheming } from "../lib/native-unstyled"; 7 | 8 | export function HeaderTestNavigationContainer() { 9 | const { reactNavigationTheme } = useTheming(); 10 | useEffect(() => { 11 | SplashScreen.hideAsync().catch(console.error); 12 | }); 13 | return ( 14 | theme={reactNavigationTheme}> 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /expo-app/src/header-test/MainTabNavigation.tsx: -------------------------------------------------------------------------------- 1 | import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; 2 | import { ParamListBase } from "@react-navigation/native"; 3 | 4 | import { HeaderDxStackNavigation } from "./HeaderDxStackNavigation"; 5 | import { NativeStackNavigation } from "./NativeStackNavigation"; 6 | 7 | export interface MainTabParamList extends ParamListBase { 8 | HeaderDxStack: undefined; 9 | NativeStack: undefined; 10 | } 11 | 12 | const MainTab = createBottomTabNavigator(); 13 | export function MainTabNavigation() { 14 | return ( 15 | 16 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /expo-app/src/header-test/NativeOneViewScreen.tsx: -------------------------------------------------------------------------------- 1 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 2 | import { Animated } from "react-native"; 3 | 4 | import { NativeStackParamList } from "./NativeStackNavigation"; 5 | import { OneViewContent } from "./OneViewContent"; 6 | import { useRefreshControl } from "../lib/native-unstyled"; 7 | 8 | export function NativeOneViewScreen({ 9 | navigation, 10 | route, 11 | }: NativeStackScreenProps) { 12 | const refreshControl = useRefreshControl( 13 | async () => new Promise((resolve) => setTimeout(resolve, 2000)) 14 | ); 15 | 16 | return ( 17 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /expo-app/src/header-test/index.ts: -------------------------------------------------------------------------------- 1 | export { HeaderTestNavigationContainer } from "./HeaderTestNavigationContainer"; 2 | -------------------------------------------------------------------------------- /expo-app/src/hooks/__mocks__/@react-native-async-storage/async-storage.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from "@react-native-async-storage/async-storage/jest/async-storage-mock"; 2 | export default AsyncStorage; 3 | -------------------------------------------------------------------------------- /expo-app/src/hooks/millisecondsBeforeNextDay.test.ts: -------------------------------------------------------------------------------- 1 | import { parseISO } from "date-fns"; 2 | import { zonedTimeToUtc } from "date-fns-tz"; 3 | 4 | import { millisecondsBeforeNextDay } from "./useDayClockState"; 5 | 6 | it("23:59:00.000 should be 60000", () => { 7 | const specimen = zonedTimeToUtc( 8 | parseISO("2022-01-01T23:59:00.000"), 9 | "America/Toronto" 10 | ); 11 | expect(millisecondsBeforeNextDay(specimen, "America/Toronto")).toBe(60000); 12 | }); 13 | 14 | it("00:00:00.000 should be 0", () => { 15 | const specimen = zonedTimeToUtc( 16 | parseISO("2022-01-01T00:00:00.000"), 17 | "America/Toronto" 18 | ); 19 | expect(millisecondsBeforeNextDay(specimen, "America/Toronto")).toBe(0); 20 | }); 21 | -------------------------------------------------------------------------------- /expo-app/src/hooks/useCachedResources.ts: -------------------------------------------------------------------------------- 1 | import { FontAwesome } from "@expo/vector-icons"; 2 | import * as Font from "expo-font"; 3 | import { FontSource } from "expo-font"; 4 | import * as SplashScreen from "expo-splash-screen"; 5 | import { useEffect, useState } from "react"; 6 | 7 | export default function useCachedResources() { 8 | const [isLoadingComplete, setLoadingComplete] = useState(false); 9 | 10 | // Load any resources or data that we need prior to rendering the app 11 | useEffect(() => { 12 | async function loadResourcesAndDataAsync() { 13 | try { 14 | SplashScreen.preventAutoHideAsync().catch(console.error); 15 | 16 | // Load fonts 17 | await Font.loadAsync({ 18 | ...FontAwesome.font, 19 | "space-mono": 20 | require("../assets/fonts/SpaceMono-Regular.ttf") as FontSource, 21 | }); 22 | } catch (e) { 23 | // We might want to provide this error information to an error reporting service 24 | console.warn(e); 25 | } finally { 26 | setLoadingComplete(true); 27 | SplashScreen.hideAsync().catch(console.error); 28 | } 29 | } 30 | 31 | loadResourcesAndDataAsync().catch(console.error); 32 | }, []); 33 | 34 | return isLoadingComplete; 35 | } 36 | -------------------------------------------------------------------------------- /expo-app/src/hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ColorSchemeName, 3 | useColorScheme as _useColorScheme, 4 | } from "react-native"; 5 | 6 | /* 7 | * The useColorScheme value is always either light or dark, but the built-in 8 | * type suggests that it can be null. This will not happen in practice, so this 9 | * makes it a bit easier to work with. 10 | */ 11 | export default function useColorScheme(): NonNullable { 12 | return _useColorScheme()!; 13 | } 14 | -------------------------------------------------------------------------------- /expo-app/src/hooks/useDayClockState.ts: -------------------------------------------------------------------------------- 1 | import { useClockState } from "@trajano/react-hooks"; 2 | import { addDays, differenceInMilliseconds, startOfDay } from "date-fns"; 3 | import { utcToZonedTime } from "date-fns-tz"; 4 | import * as Localization from "expo-localization"; 5 | 6 | /** 7 | * @param now Now in UTC 8 | * @param timeZone Time zone 9 | * @testonly 10 | */ 11 | export function millisecondsBeforeNextDay( 12 | now: number | Date, 13 | timeZone: string 14 | ): number { 15 | const nowAtZone = utcToZonedTime(now, timeZone); 16 | const diff = differenceInMilliseconds( 17 | addDays(startOfDay(nowAtZone), 1), 18 | nowAtZone 19 | ); 20 | return diff === 86400000 ? 0 : diff; 21 | } 22 | /** 23 | * This extends the useClockState but is specific for a given day based on the 24 | * local timezone 25 | */ 26 | export function useDayClockState(): Date { 27 | const defaultCalendar = Localization.getCalendars()[0]; 28 | const now = useClockState( 29 | 24 * 60 * 60 * 1000, 30 | millisecondsBeforeNextDay(Date.now(), defaultCalendar.timeZone!) 31 | ); 32 | return now; 33 | } 34 | -------------------------------------------------------------------------------- /expo-app/src/hooks/useExpoUpdateEffect.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addListener, 3 | reloadAsync, 4 | UpdateEvent, 5 | UpdateEventType, 6 | } from "expo-updates"; 7 | import { useCallback, useEffect } from "react"; 8 | 9 | import { useAlert } from "../lib/native-unstyled"; 10 | 11 | /** 12 | * This subscribes to Expo Update notifications and alerts the user when there's 13 | * a new update. This is not available on "development mode" 14 | */ 15 | export function useExpoUpdateEffect() { 16 | const { alert } = useAlert(); 17 | const checkForUpdate = useCallback( 18 | (event: UpdateEvent) => { 19 | if (event.type === UpdateEventType.UPDATE_AVAILABLE) { 20 | alert( 21 | "Update available", 22 | "An update has been downloaded and ready for use.", 23 | [ 24 | { 25 | text: "Later", 26 | style: "cancel", 27 | }, 28 | { 29 | text: "Reload", 30 | onPress: () => { 31 | reloadAsync().catch(console.error); 32 | }, 33 | }, 34 | ] 35 | ); 36 | } 37 | }, 38 | [alert] 39 | ); 40 | 41 | useEffect(() => { 42 | const sub = addListener(checkForUpdate); 43 | return () => sub.remove(); 44 | }, [checkForUpdate]); 45 | } 46 | -------------------------------------------------------------------------------- /expo-app/src/i18n/en-CA.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "Sign in", 3 | "loginAs": "Sign in as %{username}", 4 | "logout": "Sign out", 5 | "switchToDarkColorScheme": "Switch to Dark Colour Scheme", 6 | "switchToLightColorScheme": "Switch to Light Colour Scheme" 7 | } 8 | -------------------------------------------------------------------------------- /expo-app/src/i18n/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "Sign in", 3 | "loginAs": "Sign in as %{username}", 4 | "logout": "Sign out", 5 | "switchToDarkColorScheme": "Switch to Dark Color Scheme", 6 | "switchToLightColorScheme": "Switch to Light Color Scheme" 7 | } 8 | -------------------------------------------------------------------------------- /expo-app/src/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "Log in", 3 | "loginAs": "Log in as %{username}", 4 | "logout": "Log out", 5 | "switchToDarkColorScheme": "Switch to Dark Color Scheme", 6 | "switchToLightColorScheme": "Switch to Light Color Scheme" 7 | } 8 | -------------------------------------------------------------------------------- /expo-app/src/i18n/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "ログイン", 3 | "loginAs": "%{username}としてログイン", 4 | "logout": "ログアウト", 5 | "switchToDarkColorScheme": "ダークモード", 6 | "switchToLightColorScheme": "ライトモード" 7 | } 8 | -------------------------------------------------------------------------------- /expo-app/src/init/hideSplash.android.ts: -------------------------------------------------------------------------------- 1 | import * as SplashScreen from "expo-splash-screen"; 2 | SplashScreen.preventAutoHideAsync().catch(console.error); 3 | -------------------------------------------------------------------------------- /expo-app/src/init/hideSplash.ios.ts: -------------------------------------------------------------------------------- 1 | import * as SplashScreen from "expo-splash-screen"; 2 | SplashScreen.preventAutoHideAsync().catch(console.error); 3 | -------------------------------------------------------------------------------- /expo-app/src/init/hideSplash.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/expo-app/src/init/hideSplash.ts -------------------------------------------------------------------------------- /expo-app/src/init/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the app entrypoint where the root component is registered and 3 | * polyfills (if any) are set up. 4 | */ 5 | import "core-js/actual/set-immediate"; 6 | import "react-native-gesture-handler"; 7 | import "./hideSplash"; 8 | import "./layout-animation"; 9 | import "./start-network-logging"; 10 | 11 | import { registerRootComponent } from "expo"; 12 | 13 | import App from "./App"; 14 | export default registerRootComponent(App); 15 | -------------------------------------------------------------------------------- /expo-app/src/init/initialize.web.ts: -------------------------------------------------------------------------------- 1 | import "react-native-gesture-handler"; 2 | -------------------------------------------------------------------------------- /expo-app/src/init/layout-animation.android.ts: -------------------------------------------------------------------------------- 1 | import { Platform, UIManager } from "react-native"; 2 | 3 | // This is needed for LayoutAnimation https://reactnative.dev/docs/layoutanimation 4 | if (Platform.OS === "android") { 5 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 6 | if (UIManager.setLayoutAnimationEnabledExperimental) { 7 | UIManager.setLayoutAnimationEnabledExperimental(true); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /expo-app/src/init/layout-animation.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/expo-app/src/init/layout-animation.ts -------------------------------------------------------------------------------- /expo-app/src/init/sanitize-unhandled-promise-exceptions.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 2 | global.Promise = require("promise"); 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 5 | require("promise/lib/rejection-tracking").enable({ 6 | allRejections: true, 7 | onUnhandled: (id: number, error: unknown) => { 8 | if (typeof error === "object" && error instanceof Error) { 9 | // not bothering with the stack because it's useless. 10 | console.error({ 11 | name: error.name, 12 | message: error.message, 13 | cause: error.cause, 14 | }); 15 | } else { 16 | console.error( 17 | id, 18 | error, 19 | typeof error === "object", 20 | error instanceof Error 21 | ); 22 | } 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /expo-app/src/init/start-network-logging.ts: -------------------------------------------------------------------------------- 1 | import Constants from "expo-constants"; 2 | import { startNetworkLogging } from "react-native-network-logger"; 3 | 4 | const ignoredPatterns: RegExp[] = []; 5 | if (__DEV__) { 6 | try { 7 | const launchHostUrlString = Constants.manifest2?.launchAsset.url; 8 | if (launchHostUrlString) { 9 | ignoredPatterns.push(/^\w+ ${launchHostUrlString}/, /^HEAD .*/); 10 | } 11 | } catch (e: unknown) { 12 | console.warn(e); 13 | } 14 | } 15 | startNetworkLogging({ ignoredPatterns }); 16 | -------------------------------------------------------------------------------- /expo-app/src/lib/app-loading/LoadingComponentProps.ts: -------------------------------------------------------------------------------- 1 | import { ViewProps } from "react-native"; 2 | 3 | export type LoadingComponentProps = ViewProps & { 4 | totalAssets: number; 5 | loadedAssets: number; 6 | /** 7 | * This function is called to allow the loading component to specify 8 | * additional resources that it would know about. For example, the loading 9 | * component would be able to check the value of Auth context to determine if 10 | * it is in a stable state to remove the loading screen already. 11 | */ 12 | additionalResourceUpdate: (loaded: number, total: number) => void; 13 | }; 14 | -------------------------------------------------------------------------------- /expo-app/src/lib/app-loading/README.md: -------------------------------------------------------------------------------- 1 | ```tsx 2 | 9 | 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /expo-app/src/lib/app-loading/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * App Loading. This manages the splash screen for the application and handles 3 | * the asset loading. 4 | */ 5 | export { AppLoading } from "./AppLoading"; 6 | -------------------------------------------------------------------------------- /expo-app/src/lib/app-log/AppEvent.ts: -------------------------------------------------------------------------------- 1 | import { AuthState } from "@trajano/spring-docker-auth-context"; 2 | import type { AuthEvent } from "@trajano/spring-docker-auth-context"; 3 | 4 | export type AppEvent = 5 | | AuthEvent 6 | | { 7 | type: "App"; 8 | authState?: AuthState; 9 | reason: string; 10 | }; 11 | -------------------------------------------------------------------------------- /expo-app/src/lib/app-log/LoggedAuthEvent.ts: -------------------------------------------------------------------------------- 1 | import type { AppEvent } from "./AppEvent"; 2 | 3 | export type LoggedAuthEvent = AppEvent & { 4 | key: string; 5 | on: Date; 6 | }; 7 | -------------------------------------------------------------------------------- /expo-app/src/lib/app-log/index.ts: -------------------------------------------------------------------------------- 1 | export { AppLogProvider, useAppLog } from "./AppLogContext"; 2 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/ColorSwatch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Use https://json-color-palette-generator.vercel.app/ to generate the swatches 3 | * for you. Color swatches must be strings as there's no capability of swapping 4 | * OpaqueColors to strings. Or use https://github.com/arnelenero/simpler-color 5 | * to have it computed as part of your code base. 6 | */ 7 | export interface ColorSwatch { 8 | "50": string; 9 | "100": string; 10 | "200": string; 11 | "300": string; 12 | "400": string; 13 | "500": string; 14 | "600": string; 15 | "700": string; 16 | "800": string; 17 | "900": string; 18 | } 19 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/StatusBar.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar as ExpoStatusBar, StatusBarProps } from "expo-status-bar"; 2 | import type { ReactElement } from "react"; 3 | 4 | import { useTheming } from "./ThemeContext"; 5 | /** 6 | * This wraps the existing React Native StatusBar so that it is aware of the 7 | * current color scheme of the app. 8 | */ 9 | export function StatusBar(props: StatusBarProps): ReactElement { 10 | const { colorScheme } = useTheming(); 11 | return ( 12 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/StyleProps.tsx: -------------------------------------------------------------------------------- 1 | import * as RN from "react-native"; 2 | import { StyleProp } from "react-native"; 3 | 4 | /** 5 | * Style props. Deprecated values are omitted. Shadow props are also omitted in 6 | * favor of elevation 7 | */ 8 | export type StyleProps = Omit< 9 | RN.ViewStyle, 10 | | "testID" 11 | | "transformMatrix" 12 | | "rotation" 13 | | "scaleX" 14 | | "scaleY" 15 | | "translateX" 16 | | "translateY" 17 | | "shadowColor" 18 | | "shadowOffset" 19 | | "shadowOpacity" 20 | | "shadowRadius" 21 | > & { 22 | /** 23 | * If true, the existing `style` attribute will be extended. If false then the 24 | * stylings will not modify the existing style attribute. Defaults to true. 25 | */ 26 | extendStyle?: boolean; 27 | /** Existing style. */ 28 | style?: StyleProp; 29 | 30 | /** Background color alias. */ 31 | bg?: string; 32 | 33 | /** 34 | * Elevation. This replaces the shadow props and made to work the same way in 35 | * both Android and iOS when applied as a prop. 36 | */ 37 | elevation?: number; 38 | }; 39 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/StyledProps.ts: -------------------------------------------------------------------------------- 1 | import { StyleProps } from "./StyleProps"; 2 | import { TextStyleProps } from "./TextStyleProps"; 3 | 4 | export type StyledProps = StyleProps & T; 5 | export type StyledTextProps = TextStyleProps & StyleProps & T; 6 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/TextStyleProps.tsx: -------------------------------------------------------------------------------- 1 | import * as RN from "react-native"; 2 | 3 | export type TextStyleProps = Omit & { 4 | /** Foreground color. Alias for `color` */ 5 | fg?: string; 6 | /** 7 | * A type scale is a selection of font styles that can be used across an app, 8 | * ensuring a flexible, yet consistent, style that accommodates a range of 9 | * purposes. They typically provide the font, size, weight, tracking and line 10 | * height. 11 | */ 12 | typeScale?: string; 13 | /** Size associated with the role. */ 14 | size?: string; 15 | 16 | /** Alias for fontWeight: "bold" */ 17 | bold?: boolean; 18 | 19 | /** Alias for fontStyle: "italic" */ 20 | italic?: boolean; 21 | }; 22 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/Themes.ts: -------------------------------------------------------------------------------- 1 | import { TextStyle } from "react-native"; 2 | 3 | import { ColorSchemeColors } from "./ColorSchemeColors"; 4 | 5 | /** Styles that relate directly to the native Text control. */ 6 | export type ThemeTextStyle = Pick< 7 | TextStyle, 8 | | "fontFamily" 9 | | "fontWeight" 10 | | "fontSize" 11 | | "fontVariant" 12 | | "letterSpacing" 13 | | "lineHeight" 14 | | "textDecorationLine" 15 | | "textDecorationStyle" 16 | | "textDecorationColor" 17 | | "textShadowColor" 18 | | "textShadowOffset" 19 | | "textShadowRadius" 20 | | "textTransform" 21 | >; 22 | 23 | export interface ColorSchemes { 24 | /** Theme when using a dark color mode */ 25 | dark: ColorSchemeColors; 26 | /** Theme when using a light color mode */ 27 | light: ColorSchemeColors; 28 | /** Other named themes. */ 29 | [name: string]: ColorSchemeColors; 30 | } 31 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/Typography.tsx: -------------------------------------------------------------------------------- 1 | import { TextStyle } from "react-native"; 2 | 3 | /** These are the styles that represent a specific typography. */ 4 | export interface Typography { 5 | fontFamily?: TextStyle["fontFamily"]; 6 | fontWeight?: TextStyle["fontWeight"]; 7 | fontSize?: TextStyle["fontSize"]; 8 | fontStyle?: TextStyle["fontStyle"]; 9 | letterSpacing?: TextStyle["letterSpacing"]; 10 | lineHeight?: TextStyle["lineHeight"]; 11 | } 12 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/__mocks__/expo-font.ts: -------------------------------------------------------------------------------- 1 | export function isLoaded(_font: string) { 2 | return true; 3 | } 4 | export function loadAsync() { 5 | return Promise.resolve(); 6 | } 7 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/defaultColorSchemes/index.ts: -------------------------------------------------------------------------------- 1 | import { defaultDarkColorSchemeColors } from "./defaultDarkColorSchemeColors"; 2 | import { defaultLightColorSchemeColors } from "./defaultLightColorSchemeColors"; 3 | import { ColorSchemes } from "../Themes"; 4 | 5 | /** 6 | * Default color schemes. These are kept as minimal as possible as this was 7 | * meant to be extended. There are no heavy defaults that include [Apple system 8 | * colors](https://developer.apple.com/design/human-interface-guidelines/foundations/color/#specifications) 9 | * nor [design tokens](https://docs.nativebase.io/design-tokens) that I found 10 | * made NativeBase too bloated once you start getting into customization. The 11 | * number of palette colors are also limitted to the ones provided by React 12 | * Navigation. 13 | */ 14 | export const defaultColorSchemeColors: ColorSchemes = { 15 | dark: defaultDarkColorSchemeColors, 16 | light: defaultLightColorSchemeColors, 17 | }; 18 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/defaultFontTheme.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * I chose Noto-sans because it satisfies most of what I want 3 | * 4 | * - Open tail `g` 5 | * - Distinguishable capital I and lowercase l 6 | * 7 | * The flaws are: 8 | * 9 | * - There's no way to distinguish lowercase l and the pipe | symbol. 10 | * - Double-storey `a` 11 | * 12 | * It's good for the screen, but I still prefer a proper serif font for paper 13 | * and e-ink 14 | */ 15 | 16 | /** 17 | * I chose IBM Plex Sans because it satisfies most of what I want 18 | * 19 | * - Distinguishable capital I and lowercase l 20 | * - Able to to distinguish lowercase l and the pipe | symbol. 21 | * - Large variety of weights 22 | * 23 | * It's main flaws for me are 24 | * 25 | * - Double-storey `a` 26 | * - Loop-tail `g` 27 | * 28 | * It's good for the screen, but I still prefer a proper serif font for paper 29 | * and e-ink 30 | */ 31 | 32 | /** 33 | * I chose Noto Sans Mono because I don't have access to Cascadia Cove. 34 | * 35 | * - Distinguishable capital I and lowercase l 36 | * - Able to to distinguish lowercase l and the pipe | symbol. 37 | * - Open tail `g` 38 | * 39 | * It's main flaw for me are 40 | * 41 | * - Double-storey `a` 42 | * 43 | * And it's monospaced so it's function is a bit more limitted. 44 | */ 45 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/hoc/HocOptions.ts: -------------------------------------------------------------------------------- 1 | export interface HocOptions { 2 | /** Display name to use regardless. */ 3 | displayName?: string; 4 | /** Display name to use in case it cannot be determined. */ 5 | defaultDisplayName?: string; 6 | } 7 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/hoc/InputState.tsx: -------------------------------------------------------------------------------- 1 | export type InputState = "default" | "disabled" | "enabled"; 2 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/hoc/hocDisplayName.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from "react"; 2 | 3 | import { HocOptions } from "./HocOptions"; 4 | 5 | /** @param Component */ 6 | export function hocDisplayName( 7 | hocName: string, 8 | Component: ComponentType, 9 | hocOptions: HocOptions 10 | ): string | undefined { 11 | if (__DEV__) { 12 | const displayName = 13 | hocOptions.displayName ?? 14 | Component.displayName ?? 15 | Component.name ?? 16 | hocOptions.defaultDisplayName; 17 | return `${hocName}(${displayName})`; 18 | } else { 19 | return Component.displayName; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/hoc/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Higher Order Components that are used to wrap existing components to augment 3 | * styles and other things. 4 | */ 5 | export { withI18n } from "./withI18n"; 6 | export { withReplacedWithNativeFonts } from "./withReplacedWithNativeFonts"; 7 | export { withStyled } from "./withStyled"; 8 | export { withStyledScrollView } from "./withStyledScrollView"; 9 | export { withStyledSwitch } from "./withStyledSwitch"; 10 | export { withStyledText } from "./withStyledText"; 11 | export { withStyledTextInput } from "./withStyledTextInput"; 12 | export { withTextRole } from "./withTextRole"; 13 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This library provides i18n and themeing logic for React Native. It is called 3 | * unstyled as there are actually no components that are styled to look nice. 4 | * They are left as close to the default as possible. 5 | * 6 | * What it provides are wrappers to the React Native core components that give 7 | * extra props for i18n, themeing and utility. 8 | */ 9 | export type { LoadingComponentProps } from "../app-loading/LoadingComponentProps"; 10 | export type { ColorSchemeColors } from "./ColorSchemeColors"; 11 | export * from "./components"; 12 | export { defaultColorSchemeColors as defaultColorSchemes } from "./defaultColorSchemes"; 13 | export { StatusBar } from "./StatusBar"; 14 | export type { StyleProps } from "./StyleProps"; 15 | export { ThemeProvider, useColors, useTheming } from "./ThemeContext"; 16 | export type { ColorSchemes } from "./Themes"; 17 | export { useAlert } from "./useAlert"; 18 | export { useRefreshControl } from "./useRefreshControl"; 19 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/lookupColor.test.ts: -------------------------------------------------------------------------------- 1 | import { defaultLightColorSchemeColors } from "./defaultColorSchemes/defaultLightColorSchemeColors"; 2 | import { lookupColor } from "./lookupColor"; 3 | describe("lookupColor", () => { 4 | it("should lookup by layer", () => { 5 | expect(lookupColor("primary:f", defaultLightColorSchemeColors)).toBe( 6 | defaultLightColorSchemeColors.layers.primary[0] 7 | ); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/swatches/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to generate color swatches 3 | * https://donatbalipapp.medium.com/colours-maths-90346fb5abda 4 | * https://blog.logrocket.com/6-javascript-tools-color-generation/ 5 | * https://github.com/arnelenero/simpler-color 6 | */ 7 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/useAlert.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { Alert as RNAlert, AlertButton, AlertOptions } from "react-native"; 3 | 4 | import { useTheming } from "./ThemeContext"; 5 | 6 | export function useAlert(): RNAlert { 7 | const { colorScheme } = useTheming(); 8 | const alert = useCallback( 9 | ( 10 | title: string, 11 | message?: string, 12 | buttons?: AlertButton[], 13 | options?: AlertOptions 14 | ): void => { 15 | const newOptions = { 16 | userInterfaceStyle: options?.userInterfaceStyle ?? colorScheme, 17 | ...options, 18 | }; 19 | RNAlert.alert(title, message, buttons, newOptions); 20 | }, 21 | [colorScheme] 22 | ); 23 | return { alert, prompt: RNAlert.prompt }; 24 | } 25 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/useConfiguredColorScheme.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from "react"; 2 | import { ColorSchemeName, useColorScheme } from "react-native"; 3 | 4 | export function useConfiguredColorSchemes( 5 | inColorScheme: ColorSchemeName, 6 | defaultColorScheme: NonNullable, 7 | onColorSchemeChange: (nextColorScheme: ColorSchemeName) => void 8 | ): [NonNullable, (v: ColorSchemeName | null) => void] { 9 | const systemColorScheme = useColorScheme(); 10 | const [colorScheme, setColorScheme] = useState(() => { 11 | if (typeof inColorScheme === "string") { 12 | return inColorScheme; 13 | } else { 14 | return null; 15 | } 16 | }); 17 | const setColorSchemeWithNotification = useCallback( 18 | (nextColorScheme: ColorSchemeName) => { 19 | setColorScheme(nextColorScheme); 20 | onColorSchemeChange(nextColorScheme); 21 | }, 22 | [onColorSchemeChange, setColorScheme] 23 | ); 24 | 25 | const computedColorScheme = useMemo( 26 | () => (colorScheme ? colorScheme : systemColorScheme ?? defaultColorScheme), 27 | [colorScheme, systemColorScheme, defaultColorScheme] 28 | ); 29 | return [computedColorScheme, setColorSchemeWithNotification]; 30 | } 31 | -------------------------------------------------------------------------------- /expo-app/src/lib/native-unstyled/useRefreshControl.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useCallback, useState } from "react"; 2 | import { RefreshControlProps as RNRefreshControlProps } from "react-native"; 3 | 4 | import { StyledProps } from "./StyledProps"; 5 | import { RefreshControl } from "./components"; 6 | type RefreshControlProps = StyledProps< 7 | Omit 8 | > & { 9 | /** 10 | * Handles errors that may be thrown onRefresh. If not specified it writes the 11 | * error to console.error. 12 | */ 13 | onError?: (err: unknown) => void; 14 | }; 15 | /** 16 | * This is a hook that provides a simplified API for common operations on 17 | * RefreshControl. 18 | */ 19 | export function useRefreshControl( 20 | onRefresh: () => void | Promise, 21 | { onError, ...refreshControlProps }: RefreshControlProps = { 22 | onError: console.error, 23 | } 24 | ): ReactElement> { 25 | const [refreshing, setRefreshing] = useState(false); 26 | const doRefresh = useCallback(() => { 27 | setRefreshing(true); 28 | Promise.resolve(onRefresh()) 29 | .catch(onError) 30 | .finally(() => setRefreshing(false)); 31 | }, [onRefresh, onError]); 32 | 33 | return ( 34 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /expo-app/src/lib/opinionated-design/README.md: -------------------------------------------------------------------------------- 1 | # Opinionated Design component library 2 | 3 | I just like it this way. But I may add a few reasonings why. 4 | 5 | The basis for the most part is [Fluent UI](https://react.fluentui.dev/) where I sort of like the general flatness because I want focus to be part of the content. I also dropped any notion of making it look like the platform. 6 | 7 | There is less animation that is not driven by user action. So only "hover", "blur", "focus", "pressing" when you have an indirect input like mouse. "focus", "pressing" only occurs on touch. 8 | 9 | There are two primary components that would be defined: Fields and Buttons 10 | 11 | ## Fields 12 | 13 | Fields (which are actually derived from Material UI because I like that the field names are embedded in the field as it makes better use of screen space) but minus the grow-shrink effect of the label on focus. 14 | 15 | ## Button 16 | 17 | Buttons which are predominantly Fluent UI, but with a few tweaks. 18 | * Focus should not be drawn too much to the button unless it's a CTA. When you're dealing with form entry, your focus should be the form and not the submit button. 19 | * As such it shouldn't be another eyes drawn in background color. 20 | 21 | But that can be done on the usage. However, the description of "primary" will likely be swapped with "standard" (or undefined), "attention" and "danger" 22 | -------------------------------------------------------------------------------- /expo-app/src/lib/stack-navigator-header-dx/NonNativeStackView.tsx: -------------------------------------------------------------------------------- 1 | import { ParamListBase, StackNavigationState } from "@react-navigation/native"; 2 | import { StackNavigationProp } from "@react-navigation/stack"; 3 | import { View, ViewProps } from "react-native"; 4 | export function NonNativeStackView({ 5 | state, 6 | navigation, 7 | descriptors, 8 | ...rest 9 | }: ViewProps & { 10 | state: StackNavigationState; 11 | navigation: StackNavigationProp; 12 | descriptors: Record; 13 | }) { 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /expo-app/src/lib/stack-navigator-header-dx/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a header that is meant to be used with Stack navigator. It's more of 3 | * a template rather than a full header UI. But will allow various animations to 4 | * be put on top of it. 5 | * 6 | * The general concept is a Stack Navigator Screen would have the following 7 | * components 8 | * 9 | * - Header Area 10 | * 11 | * - Refresh zone 12 | * - Header title zone 13 | * - Large header title zone 14 | * - Notification zone 15 | * - Content Area (which is usually a scroll view and will be adjusted based on 16 | * the header area) 17 | */ 18 | export { HeaderDx } from "./HeaderDx"; 19 | export { HeaderDxProvider } from "./HeaderDxContext"; 20 | export { HeaderDxLarge } from "./HeaderDxLarge"; 21 | -------------------------------------------------------------------------------- /expo-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "expo/tsconfig.base", 4 | "compilerOptions": { 5 | "strict": true, 6 | "allowJs": false, 7 | "module": "ESNext" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /expo-app/types.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about using TypeScript with React Navigation: 3 | * https://reactnavigation.org/docs/typescript/ 4 | */ 5 | 6 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 7 | 8 | import { RootStackParamList } from "./navigation/paramLists"; 9 | 10 | declare global { 11 | namespace ReactNavigation { 12 | interface RootParamList extends RootStackParamList {} 13 | } 14 | } 15 | 16 | export type RootStackScreenProps = 17 | NativeStackScreenProps; 18 | -------------------------------------------------------------------------------- /expo-app/webpack.config.js: -------------------------------------------------------------------------------- 1 | const createExpoWebpackConfigAsync = require('@expo/webpack-config'); 2 | 3 | module.exports = async function (env, argv) { 4 | const config = await createExpoWebpackConfigAsync(env, argv); 5 | config.resolve.alias["lottie-react-native"] = "react-native-web-lottie"; 6 | // Customize the config before returning it. 7 | return config; 8 | }; 9 | -------------------------------------------------------------------------------- /gateway-common/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /gateway-common/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'io.spring.dependency-management' version '1.1.0' 3 | id 'jacoco' 4 | id 'net.trajano.swarm.conventions' 5 | id 'java-library' 6 | } 7 | 8 | group = 'net.trajano.swarm' 9 | version = '0.0.1-SNAPSHOT' 10 | 11 | configurations { 12 | compileOnly { 13 | extendsFrom annotationProcessor 14 | } 15 | } 16 | 17 | jar { 18 | enabled = true 19 | } 20 | dependencies { 21 | api 'org.springframework.boot:spring-boot' 22 | api 'org.springframework.data:spring-data-redis' 23 | api 'org.bitbucket.b_c:jose4j:0.9.3' 24 | api 'io.micrometer:micrometer-observation' 25 | compileOnly 'org.projectlombok:lombok' 26 | annotationProcessor 'org.projectlombok:lombok' 27 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 28 | } 29 | 30 | dependencyManagement { 31 | imports { 32 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 33 | mavenBom "org.springframework.boot:spring-boot-dependencies:${springBootVersion}" 34 | } 35 | } 36 | 37 | tasks.named('test') { 38 | useJUnitPlatform() 39 | } 40 | -------------------------------------------------------------------------------- /gateway-common/src/main/java/net/trajano/swarm/gateway/converters/Converters.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.converters; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | 5 | @ComponentScan 6 | public class Converters {} 7 | -------------------------------------------------------------------------------- /gateway-common/src/main/java/net/trajano/swarm/gateway/converters/JsonWebKeySetToStringConverter.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.converters; 2 | 3 | import org.jose4j.jwk.JsonWebKeySet; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.data.convert.WritingConverter; 6 | import org.springframework.lang.Nullable; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | @WritingConverter 11 | public class JsonWebKeySetToStringConverter implements Converter { 12 | 13 | @Nullable @Override 14 | public String convert(final JsonWebKeySet source) { 15 | 16 | return source.toJson(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gateway-common/src/main/java/net/trajano/swarm/gateway/converters/JsonWebKeyToStringConverter.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.converters; 2 | 3 | import org.jose4j.jwk.JsonWebKey; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.data.convert.WritingConverter; 6 | import org.springframework.lang.Nullable; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | @WritingConverter 11 | public class JsonWebKeyToStringConverter implements Converter { 12 | 13 | @Nullable @Override 14 | public String convert(final JsonWebKey source) { 15 | 16 | return source.toJson(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gateway-common/src/main/java/net/trajano/swarm/gateway/converters/JwtClaimsToStringConverter.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.converters; 2 | 3 | import org.jose4j.jwt.JwtClaims; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.data.convert.WritingConverter; 6 | import org.springframework.lang.Nullable; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | @WritingConverter 11 | public class JwtClaimsToStringConverter implements Converter { 12 | 13 | @Nullable @Override 14 | public String convert(final JwtClaims source) { 15 | 16 | return source.toJson(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gateway-common/src/main/java/net/trajano/swarm/gateway/converters/StringToJsonWebKeyConverter.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.converters; 2 | 3 | import org.jose4j.jwk.JsonWebKey; 4 | import org.jose4j.lang.JoseException; 5 | import org.springframework.core.convert.converter.Converter; 6 | import org.springframework.data.convert.ReadingConverter; 7 | import org.springframework.lang.Nullable; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | @ReadingConverter 12 | public class StringToJsonWebKeyConverter implements Converter { 13 | 14 | @Nullable @Override 15 | public JsonWebKey convert(final String source) { 16 | 17 | try { 18 | return JsonWebKey.Factory.newJwk(source); 19 | } catch (JoseException e) { 20 | throw new IllegalArgumentException(e); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gateway-common/src/main/java/net/trajano/swarm/gateway/converters/StringToJsonWebKeySetConverter.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.converters; 2 | 3 | import org.jose4j.jwk.JsonWebKeySet; 4 | import org.jose4j.lang.JoseException; 5 | import org.springframework.core.convert.converter.Converter; 6 | import org.springframework.data.convert.ReadingConverter; 7 | import org.springframework.lang.Nullable; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | @ReadingConverter 12 | public class StringToJsonWebKeySetConverter implements Converter { 13 | 14 | @Nullable @Override 15 | public JsonWebKeySet convert(final String source) { 16 | 17 | try { 18 | return new JsonWebKeySet(source); 19 | } catch (JoseException e) { 20 | throw new IllegalArgumentException(e); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gateway-common/src/main/java/net/trajano/swarm/gateway/converters/StringToJwtClaimsConverter.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.converters; 2 | 3 | import org.jose4j.jwt.JwtClaims; 4 | import org.jose4j.jwt.consumer.InvalidJwtException; 5 | import org.springframework.core.convert.converter.Converter; 6 | import org.springframework.data.convert.ReadingConverter; 7 | import org.springframework.lang.Nullable; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | @ReadingConverter 12 | public class StringToJwtClaimsConverter implements Converter { 13 | 14 | @Nullable @Override 15 | public JwtClaims convert(final String source) { 16 | 17 | try { 18 | return JwtClaims.parse(source); 19 | } catch (InvalidJwtException e) { 20 | throw new IllegalArgumentException(e); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gateway-common/src/main/java/net/trajano/swarm/gateway/healthcheck/package-info.java: -------------------------------------------------------------------------------- 1 | /** Provides a probe main method that would use Spring Actuator Healthcheck via JMX. */ 2 | package net.trajano.swarm.gateway.healthcheck; 3 | -------------------------------------------------------------------------------- /gateway-common/src/main/java/net/trajano/swarm/gateway/redis/package-info.java: -------------------------------------------------------------------------------- 1 | /** Spring Data Redis support files. */ 2 | package net.trajano.swarm.gateway.redis; 3 | -------------------------------------------------------------------------------- /gateway-common/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gateway/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/ExcludedPathPatterns.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway; 2 | 3 | import java.util.List; 4 | import java.util.stream.Stream; 5 | import org.springframework.http.server.PathContainer; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.util.pattern.PathPattern; 8 | import org.springframework.web.util.pattern.PathPatternParser; 9 | 10 | @Component 11 | public final class ExcludedPathPatterns { 12 | 13 | private final List excludedServerPathPatterns = 14 | Stream.of("/actuator/**", "/ping", "/favicon.ico", "**/*.css", "**/*.js", "**/*.html") 15 | .map(p -> new PathPatternParser().parse(p)) 16 | .toList(); 17 | 18 | public boolean isExcludedForServer(final PathContainer pathContainer) { 19 | 20 | return excludedServerPathPatterns.stream() 21 | .anyMatch(pathPattern -> pathPattern.matches(pathContainer)); 22 | } 23 | 24 | public boolean isExcludedForServer(final String path) { 25 | 26 | return isExcludedForServer(PathContainer.parsePath(path)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/LoggingThreadFactory.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway; 2 | 3 | import java.util.concurrent.ThreadFactory; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | @Slf4j 8 | @RequiredArgsConstructor 9 | public class LoggingThreadFactory implements ThreadFactory { 10 | 11 | private final ThreadFactory delegate; 12 | 13 | @Override 14 | public Thread newThread(Runnable r) { 15 | 16 | return delegate.newThread(new LoggingRunnable(r)); 17 | } 18 | 19 | @RequiredArgsConstructor 20 | static class LoggingRunnable implements Runnable { 21 | private final Runnable runnableDelegate; 22 | 23 | @Override 24 | public void run() { 25 | final long start = System.currentTimeMillis(); 26 | try { 27 | runnableDelegate.run(); 28 | } finally { 29 | final var time = System.currentTimeMillis() - start; 30 | if (time > 1000) { 31 | log.error("{} took {} ms", runnableDelegate, time); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/ObservabilityConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway; 2 | 3 | import io.lettuce.core.resource.ClientResources; 4 | import io.micrometer.observation.ObservationRegistry; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.data.redis.connection.lettuce.observability.MicrometerTracingAdapter; 10 | 11 | @Configuration 12 | class ObservabilityConfiguration { 13 | 14 | @Bean 15 | public ClientResources clientResources( 16 | ObservationRegistry observationRegistry, 17 | @Value("${spring.data.redis.host:redis}") final String redisServiceName) { 18 | 19 | return ClientResources.builder() 20 | .tracing(new MicrometerTracingAdapter(observationRegistry, redisServiceName)) 21 | .build(); 22 | } 23 | 24 | @Bean 25 | public LettuceClientConfigurationBuilderCustomizer 26 | observabilityLettuceClientConfigurationBuilderCustomizer(ClientResources clientResources) { 27 | 28 | return clientConfigurationBuilder -> { 29 | clientConfigurationBuilder.clientResources(clientResources); 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/ServerWebExchangeAttributes.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 7 | public final class ServerWebExchangeAttributes { 8 | 9 | public static final String JWT_ID = "jwtId"; 10 | 11 | /** JWT Claims. {@link org.jose4j.jwt.JwtClaims}. */ 12 | public static final String JWT_CLAIMS = "jwtClaims"; 13 | } 14 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/AuthControllerMappingsProperties.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix = "auth.controller-mappings") 9 | @Data 10 | public class AuthControllerMappingsProperties { 11 | 12 | /** Mapping for the authentication endpoint */ 13 | private String authentication; 14 | 15 | /** Mapping for the jwks endpoint. */ 16 | private String jwks; 17 | 18 | /** Mapping for the logout endpoint. */ 19 | private String logout; 20 | 21 | /** Mapping for the refresh endpoint. */ 22 | private String refresh; 23 | 24 | /** Mapping for the userProfile endpoint. */ 25 | private String userProfile; 26 | } 27 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/AuthCredentialStorage.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth; 2 | 3 | public interface AuthCredentialStorage {} 4 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/AuthServiceResponse.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth; 2 | 3 | import java.time.Duration; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import net.trajano.swarm.gateway.web.GatewayResponse; 8 | import org.springframework.http.HttpStatus; 9 | 10 | @Data 11 | @Builder 12 | @AllArgsConstructor 13 | public class AuthServiceResponse { 14 | 15 | /** this is the actual response to send back to the client */ 16 | private R operationResponse; 17 | 18 | /** Duration to delay a mono output by. */ 19 | @Builder.Default private Duration delay = Duration.ZERO; 20 | 21 | /** HTTP Status. */ 22 | @Builder.Default private HttpStatus statusCode = HttpStatus.OK; 23 | } 24 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/AuthenticationContext.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class AuthenticationContext { 7 | private String clientId; 8 | } 9 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/OAuthRefreshRequest.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class OAuthRefreshRequest { 7 | 8 | /** 9 | * Refresh token. Used non-conventional method due to limitation of Spring. 10 | * https://github.com/spring-projects/spring-framework/issues/18012 11 | */ 12 | private String refresh_token; 13 | /** 14 | * Grant type, should be refresh_token. Used non-conventional method due to limitation of Spring. 15 | * https://github.com/spring-projects/spring-framework/issues/18012 16 | */ 17 | private String grant_type; 18 | } 19 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/OAuthRevocationRequest.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class OAuthRevocationRequest { 7 | 8 | /** Refresh token. */ 9 | private String token; 10 | 11 | /** 12 | * Token type hint, should be refresh_token. Used non-conventional method due to limitation of 13 | * Spring. https://github.com/spring-projects/spring-framework/issues/18012 14 | */ 15 | private String token_type_hint; 16 | } 17 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/clientmanagement/ClientManagementConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth.clientmanagement; 2 | 3 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | public class ClientManagementConfiguration { 9 | 10 | @Bean 11 | @ConditionalOnMissingBean 12 | ClientManagementService noCheckClientManagementService() { 13 | return new NoCheckClientManagementService(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/clientmanagement/InvalidClientException.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth.clientmanagement; 2 | 3 | public class InvalidClientException extends SecurityException { 4 | 5 | public InvalidClientException() {} 6 | 7 | public InvalidClientException(String s) { 8 | 9 | super(s); 10 | } 11 | 12 | public InvalidClientException(String message, Throwable cause) { 13 | 14 | super(message, cause); 15 | } 16 | 17 | public InvalidClientException(Throwable cause) { 18 | 19 | super(cause); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/clientmanagement/NoCheckClientManagementService.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth.clientmanagement; 2 | 3 | import org.springframework.http.HttpHeaders; 4 | import reactor.core.publisher.Mono; 5 | 6 | /** This skips all checks. It will work even if the authorization is missing from the header. */ 7 | public class NoCheckClientManagementService implements ClientManagementService { 8 | 9 | public static final String UNKNOWN = "unknown"; 10 | 11 | @Override 12 | public Mono obtainClientIdFromAuthorization(String authorization) { 13 | 14 | return Mono.just(UNKNOWN); 15 | } 16 | 17 | @Override 18 | public Mono obtainClientId(String clientId, String clientSecret) { 19 | 20 | return Mono.just(UNKNOWN); 21 | } 22 | 23 | @Override 24 | public Mono obtainClientIdFromHeaders(HttpHeaders headers) { 25 | 26 | return Mono.just(UNKNOWN); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/oidc/ReactiveOidcService.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth.oidc; 2 | 3 | import java.net.URI; 4 | import org.jose4j.jwt.JwtClaims; 5 | import reactor.core.publisher.Flux; 6 | import reactor.core.publisher.Mono; 7 | 8 | /** 9 | * Provides OIDC support services for use with the {@link 10 | * net.trajano.swarm.gateway.auth.IdentityService} 11 | */ 12 | public interface ReactiveOidcService { 13 | 14 | /** 15 | * Gets a flux of allowed issuers. This is from a comma separated list of 16 | * auth.oidc.allowed-issuers 17 | * 18 | * @return allowed issuers. 19 | */ 20 | Flux allowedIssuers(); 21 | 22 | /** 23 | * This obtains the claims from the issuer. 24 | * 25 | * @param issuer issuer 26 | * @param accessToken access token provided by the IP to get the user info. 27 | * @return claims 28 | */ 29 | Mono getClaims(URI issuer, String accessToken); 30 | } 31 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/oidc/WellKnownOpenIdConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth.oidc; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import java.net.URI; 6 | import lombok.Data; 7 | 8 | @Data 9 | @JsonIgnoreProperties(ignoreUnknown = true) 10 | public class WellKnownOpenIdConfiguration { 11 | private URI issuer; 12 | 13 | @JsonProperty("userinfo_endpoint") 14 | private URI userinfoEndpoint; 15 | 16 | @JsonProperty("jwks_uri") 17 | private URI jwksUri; 18 | } 19 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/oidc/package-info.java: -------------------------------------------------------------------------------- 1 | /** OIDC specific classes */ 2 | package net.trajano.swarm.gateway.auth.oidc; 3 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This provides endpoints that implement a portion of the OAuth 2.0 APIs and provide a filter to do 3 | * protected resources. 4 | */ 5 | package net.trajano.swarm.gateway.auth; 6 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/simple/AuthenticationItem.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth.simple; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import lombok.*; 6 | import org.jose4j.jwk.JsonWebKeySet; 7 | import org.jose4j.jwt.JwtClaims; 8 | 9 | /** Rather than passing tuples this holds the data per stage */ 10 | @Data 11 | @Builder 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | @With 15 | public class AuthenticationItem { 16 | 17 | private SimpleAuthenticationRequest authenticationRequest; 18 | private JsonWebKeySet jwks; 19 | private JwtClaims jwtClaims; 20 | @Builder.Default private Map secret = new HashMap<>(); 21 | private String accessToken; 22 | 23 | /** Holds the Refresh token, may be signed or unsigned. */ 24 | private String refreshToken; 25 | } 26 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/simple/SimpleAuthController.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth.simple; 2 | 3 | import net.trajano.swarm.gateway.auth.AbstractAuthController; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @RestController 8 | @ConditionalOnProperty(name = "simple-auth.enabled", havingValue = "true") 9 | public class SimpleAuthController 10 | extends AbstractAuthController {} 11 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/simple/SimpleAuthServiceConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth.simple; 2 | 3 | import net.trajano.swarm.gateway.auth.oidc.ReactiveOidcService; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | @ConditionalOnProperty(name = "simple-auth.enabled", havingValue = "true") 10 | public class SimpleAuthServiceConfiguration { 11 | 12 | @Bean 13 |

SimpleIdentityService

simpleAuthService( 14 | final ReactiveOidcService reactiveOidcService, 15 | final SimpleAuthServiceProperties simpleAuthServiceProperties) { 16 | 17 | return new SimpleIdentityService<>(reactiveOidcService); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/simple/SimpleAuthenticationRequest.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth.simple; 2 | 3 | import java.net.URI; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import org.springframework.lang.Nullable; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @Builder 14 | public class SimpleAuthenticationRequest { 15 | 16 | /** Username of the person. */ 17 | private String username; 18 | 19 | /** Indicates that the user is to be authenticated successfully. */ 20 | private boolean authenticated; 21 | 22 | /** Allow request to alter the access token expiration (for testing) */ 23 | @Nullable private Long accessTokenExpiresInMillis; 24 | /** Allow request to alter the refresh token expiration (for testing) */ 25 | @Nullable private Long refreshTokenExpiresInMillis; 26 | 27 | /** For OIDC login, this is the issuer. */ 28 | private URI issuer; 29 | /** For OIDC login, this is the access token which should be a JWT. */ 30 | private String accessToken; 31 | } 32 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/auth/simple/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a simplistic implementation of the auth service used for verification. It uses Redis to 3 | * store the keys. Use this only for testing or inspiration. 4 | */ 5 | package net.trajano.swarm.gateway.auth.simple; 6 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/datasource/redis/RefreshContext.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.datasource.redis; 2 | 3 | import java.time.Instant; 4 | import lombok.*; 5 | import net.trajano.swarm.gateway.auth.IdentityServiceResponse; 6 | import net.trajano.swarm.gateway.redis.UserSession; 7 | import org.jose4j.jwk.JsonWebKeySet; 8 | import org.jose4j.jwt.JwtClaims; 9 | 10 | @Data 11 | @With 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | @Builder 15 | public class RefreshContext { 16 | 17 | /** Signed access token. */ 18 | private String accessToken; 19 | 20 | private JwtClaims accessTokenClaims; 21 | 22 | private Instant accessTokenExpiresAt; 23 | 24 | private JsonWebKeySet accessTokenSigningKeyPair; 25 | 26 | private String clientId; 27 | 28 | private IdentityServiceResponse identityServiceResponse; 29 | 30 | private String jwtId; 31 | 32 | private Instant now; 33 | 34 | /** Signed refresh token. */ 35 | private String refreshToken; 36 | 37 | private JwtClaims refreshTokenClaims; 38 | 39 | private Instant refreshTokenExpiresAt; 40 | 41 | private JsonWebKeySet refreshTokenSigningKeyPair; 42 | 43 | private UserSession userSession; 44 | } 45 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/discovery/DockerDiscoveryProperties.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.discovery; 2 | 3 | import java.util.List; 4 | import lombok.Data; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | @ConfigurationProperties(prefix = "docker.discovery") 10 | @Data 11 | public class DockerDiscoveryProperties { 12 | 13 | /** Label prefix to process. */ 14 | private String labelPrefix = "docker"; 15 | 16 | /** Network to scan services/containers on. */ 17 | private String network = "services"; 18 | 19 | /** Swarm mode. If true, it will scan services rather than containers. */ 20 | private boolean swarmMode = false; 21 | 22 | /** Indicates where the connection to the Docker daemon has full permissions. */ 23 | private boolean daemonFullAccess = true; 24 | 25 | public List idsLabelFilter() { 26 | 27 | return List.of(idsLabel()); 28 | } 29 | 30 | public String idsLabel() { 31 | 32 | return labelPrefix + ".ids"; 33 | } 34 | 35 | public String idLabel() { 36 | 37 | return labelPrefix + ".id"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/discovery/DockerServiceInstanceBuilder.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.discovery; 2 | 3 | import com.github.dockerjava.api.model.Service; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.cloud.client.ServiceInstance; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | @RequiredArgsConstructor 10 | public class DockerServiceInstanceBuilder { 11 | 12 | private final DockerDiscoveryProperties dockerDiscoveryProperties; 13 | 14 | public ServiceInstance build(Service service, String serviceId, String address) { 15 | return new DockerServiceInstance( 16 | service, dockerDiscoveryProperties.getLabelPrefix(), serviceId, address); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/discovery/ratelimiter/DiscoveryRequestRateLimiterGatewayFilterFactory.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.discovery.ratelimiter; 2 | 3 | import org.springframework.beans.factory.annotation.Qualifier; 4 | import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory; 5 | import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; 6 | import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public class DiscoveryRequestRateLimiterGatewayFilterFactory 11 | extends RequestRateLimiterGatewayFilterFactory { 12 | 13 | public DiscoveryRequestRateLimiterGatewayFilterFactory( 14 | RateLimiter defaultRateLimiter, 15 | @Qualifier("tokenKeyResolver") KeyResolver defaultKeyResolver) { 16 | 17 | super(defaultRateLimiter, defaultKeyResolver); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/docker/DockerProperties.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.docker; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | @ConfigurationProperties(prefix = "docker") 9 | @Data 10 | public class DockerProperties { 11 | 12 | private String host = "unix:///var/run/docker.sock"; 13 | } 14 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/grpc/DataBufferFluxInputStream.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.grpc; 2 | 3 | import java.io.SequenceInputStream; 4 | import java.util.Collections; 5 | import org.springframework.core.io.buffer.DataBuffer; 6 | import reactor.core.publisher.Flux; 7 | 8 | public class DataBufferFluxInputStream extends SequenceInputStream { 9 | public DataBufferFluxInputStream(Flux publisher, boolean releaseOnClose) { 10 | 11 | super( 12 | publisher 13 | .map(dataBuffer -> dataBuffer.asInputStream(releaseOnClose)) 14 | .collectList() 15 | .map(Collections::enumeration) 16 | .block()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/grpc/GrpcHealthIndicator.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.grpc; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.boot.actuate.health.Health; 5 | import org.springframework.boot.actuate.health.ReactiveHealthIndicator; 6 | import org.springframework.boot.actuate.health.Status; 7 | import org.springframework.stereotype.Component; 8 | import reactor.core.publisher.Mono; 9 | 10 | /** Checks if the current or previous signing keys are present. */ 11 | @Component 12 | @RequiredArgsConstructor 13 | public class GrpcHealthIndicator implements ReactiveHealthIndicator { 14 | 15 | private final ChannelProvider channelProvider; 16 | 17 | @Override 18 | public Mono health() { 19 | 20 | return Mono.fromSupplier( 21 | () -> { 22 | final var channels = channelProvider.getChannels(); 23 | return Health.status(Status.UP).withDetails(channels).build(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/grpc/JwtCallCredentials.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.grpc; 2 | 3 | import io.grpc.CallCredentials; 4 | import io.grpc.Metadata; 5 | import java.util.concurrent.Executor; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | /** Provides the JWT into the meta data */ 9 | @RequiredArgsConstructor 10 | public class JwtCallCredentials extends CallCredentials { 11 | 12 | // JWT key, may pass the key to use. 13 | public static final Metadata.Key JWT_CLAIMS_KEY = 14 | Metadata.Key.of("jwtClaims", Metadata.ASCII_STRING_MARSHALLER); 15 | 16 | private final String jwt; 17 | 18 | @Override 19 | public void applyRequestMetadata( 20 | RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) { 21 | 22 | appExecutor.execute( 23 | () -> { 24 | final var metadata = new Metadata(); 25 | metadata.put(JWT_CLAIMS_KEY, jwt); 26 | applier.apply(metadata); 27 | }); 28 | } 29 | 30 | @Override 31 | public void thisUsesUnstableApi() { 32 | // no-op 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/grpc/MethodDescriptorCacheKey.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.grpc; 2 | 3 | import java.net.URI; 4 | 5 | public record MethodDescriptorCacheKey(String serviceInstanceId, URI uri) {} 6 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/jwks/JwksProvider.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.jwks; 2 | 3 | import java.time.Duration; 4 | import org.jose4j.jwk.JsonWebKeySet; 5 | import reactor.core.publisher.Mono; 6 | import reactor.util.function.Tuple2; 7 | 8 | /** This provides the JWKs used for JWT signing and encryption. */ 9 | public interface JwksProvider { 10 | 11 | Mono getSigningKey(int accessTokenExpirationInSeconds); 12 | 13 | /** 14 | * Returns the active JSON Web Key set excluding private keys. This set is returned to /jwks and 15 | * is used for access token validation. 16 | * 17 | * @return Json web key set 18 | */ 19 | Mono jsonWebKeySet(); 20 | 21 | Mono> jsonWebKeySetWithDuration(); 22 | } 23 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/web/GatewayResponse.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.web; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @Builder 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @JsonInclude(JsonInclude.Include.NON_NULL) 14 | public class GatewayResponse { 15 | private boolean ok; 16 | private String error; 17 | private String errorDescription; 18 | } 19 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/web/InvalidClientGatewayResponse.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.web; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.ToString; 7 | 8 | @Data 9 | @EqualsAndHashCode(callSuper = true) 10 | @ToString(callSuper = true) 11 | @JsonInclude(JsonInclude.Include.NON_NULL) 12 | public class InvalidClientGatewayResponse extends GatewayResponse { 13 | 14 | public InvalidClientGatewayResponse() { 15 | 16 | super(false, "invalid_client", null); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/web/OrderedGlobalFilter.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.web; 2 | 3 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 4 | import org.springframework.cloud.gateway.filter.GlobalFilter; 5 | import org.springframework.core.Ordered; 6 | import org.springframework.web.server.ServerWebExchange; 7 | import reactor.core.publisher.Mono; 8 | 9 | class OrderedGlobalFilter implements Ordered, GlobalFilter { 10 | 11 | private final GlobalFilter delegate; 12 | 13 | private final int order; 14 | 15 | public OrderedGlobalFilter(GlobalFilter delegate, int order) { 16 | 17 | this.delegate = delegate; 18 | this.order = order; 19 | } 20 | 21 | @Override 22 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { 23 | 24 | System.out.println("DELEGATE:" + exchange.getAttributes()); 25 | 26 | return delegate.filter(exchange, chain); 27 | } 28 | 29 | @Override 30 | public int getOrder() { 31 | 32 | return order; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gateway/src/main/java/net/trajano/swarm/gateway/web/UnauthorizedGatewayResponse.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.web; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.ToString; 7 | 8 | @Data 9 | @EqualsAndHashCode(callSuper = true) 10 | @ToString(callSuper = true) 11 | @JsonInclude(JsonInclude.Include.NON_NULL) 12 | public class UnauthorizedGatewayResponse extends GatewayResponse { 13 | 14 | public UnauthorizedGatewayResponse() { 15 | 16 | this(null); 17 | } 18 | 19 | public UnauthorizedGatewayResponse(String description) { 20 | 21 | super(false, "invalid_token", description); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gateway/src/main/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | cloud: 2 | aws: 3 | enabled: false 4 | simple-auth: 5 | enabled: true 6 | 7 | logging: 8 | level: 9 | com.amazonaws.util.EC2MetadataUtils: ERROR -------------------------------------------------------------------------------- /gateway/src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/gateway/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /gateway/src/main/resources/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | 4 | -------------------------------------------------------------------------------- /gateway/src/test/java/net/trajano/swarm/gateway/FluxPoolTest.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import reactor.core.publisher.Mono; 5 | import reactor.test.StepVerifier; 6 | 7 | class FluxPoolTest { 8 | 9 | @Test 10 | void filterThen() { 11 | 12 | var mono = Mono.just(false).filter(i -> i); 13 | StepVerifier.create(mono).expectComplete().verify(); 14 | } 15 | 16 | @Test 17 | void filterThen2() { 18 | 19 | var mono = 20 | Mono.just("mykey") 21 | .flatMap( 22 | mykey -> 23 | Mono.just(false) 24 | .filter(i -> i) 25 | .switchIfEmpty(Mono.error(IllegalStateException::new)) 26 | .thenReturn(mykey)) 27 | .flatMap(myKey -> Mono.just("SHOULD NOT GET HERE")); 28 | StepVerifier.create(mono).expectNextCount(0).expectError().verify(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /gateway/src/test/java/net/trajano/swarm/gateway/auth/simple/UriTest.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.auth.simple; 2 | 3 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 4 | 5 | import java.net.URI; 6 | import net.trajano.swarm.gateway.auth.oidc.WellKnownReactiveOidcService; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class UriTest { 10 | 11 | @Test 12 | void resolve() { 13 | var google = URI.create("https://accounts.google.com"); 14 | var r = WellKnownReactiveOidcService.wellKnownOpenIdConfigurationUri(google); 15 | assertThat(r) 16 | .isEqualTo(URI.create("https://accounts.google.com/.well-known/openid-configuration")); 17 | } 18 | 19 | @Test 20 | void resolveTenant() { 21 | var microsoft = URI.create("https://login.microsoftonline.com/tenant-id"); 22 | var r = WellKnownReactiveOidcService.wellKnownOpenIdConfigurationUri(microsoft); 23 | assertThat(r) 24 | .isEqualTo( 25 | URI.create( 26 | "https://login.microsoftonline.com/tenant-id/.well-known/openid-configuration")); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gateway/src/test/java/net/trajano/swarm/gateway/datasource/redis/RedisJtiExtractorServiceTest.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.gateway.datasource.redis; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | class RedisJtiExtractorServiceTest { 9 | 10 | @Test 11 | void extractJtiWithoutValidation() { 12 | var jwtId = 13 | RedisJtiExtractorService.extractJtiWithoutValidation( 14 | "DPiF.eyJqdGkiOiJmY2U5MTEzZi02NjQyLTQ5MDQtODEwMy1jNTdhNzg1ZDU1OWIiLCJleHAiOjE2NjIyNjUxODh9.YQpsZgeCAXnEEcxhJnAlGrujcJtflwvntGu9i6WkbN0uUTb3mwRZaeeIAEoo3kydCh4Rk3BtI8epXmYvNJMEi3S7kks6j9VFx5I26zggGm4qlqHIkYAhHxOKIfnAomxfskhOrtMrCSL-4NI7ejJFilNVjO1sGQyxjHO24Wv4dhSYeLyqtSiyukZUgQC98TVfstviQX6n-j2f2gOoV_ZDDgkUSAd2zOqlIuOFdS79OSLRF0Dfn-qmag8jlkgLfBF0v_uhn72MvXWisIsmF66VYy_18x-ZvCQsusgHKjsnQ1JWyctDMnTQMwxXGOTnF1uWjARf1_5oCZWE6YsoZLVDiA"); 15 | assertThat(jwtId).hasToString("fce9113f-6642-4904-8103-c57a785d559b"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gateway/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /greclipse.prefs: -------------------------------------------------------------------------------- 1 | org.eclipse.jdt.core.formatter.tabulation.char=space 2 | org.eclipse.jdt.core.formatter.indent_empty_lines=false 3 | -------------------------------------------------------------------------------- /grpc-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /grpc-service/src/main/java/net/trajano/swarm/sampleservice/SampleServiceApplication.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.sampleservice; 2 | 3 | import java.io.IOException; 4 | import java.io.UncheckedIOException; 5 | import java.util.concurrent.Executors; 6 | import org.springframework.beans.factory.InitializingBean; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | 11 | @SpringBootApplication 12 | public class SampleServiceApplication implements InitializingBean { 13 | 14 | @Autowired private GrpcServer grpcServer; 15 | 16 | public static void main(String[] args) { 17 | 18 | SpringApplication.run(SampleServiceApplication.class, args); 19 | } 20 | 21 | @Override 22 | public void afterPropertiesSet() { 23 | 24 | final var executorService = Executors.newSingleThreadExecutor(); 25 | executorService.submit( 26 | () -> { 27 | try { 28 | grpcServer.start().awaitTermination(); 29 | } catch (InterruptedException e) { 30 | Thread.currentThread().interrupt(); 31 | } catch (IOException e) { 32 | throw new UncheckedIOException(e); 33 | } 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /grpc-service/src/main/proto/echo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package net.trajano.swarm.sampleservice; 3 | import "import1.proto"; 4 | import "google/protobuf/struct.proto"; 5 | import "google/protobuf/type.proto"; 6 | 7 | message EchoRequest { 8 | string message = 1; 9 | } 10 | 11 | message EchoResponse { 12 | string message = 1; 13 | /* 14 | * JWT Claims destructured to a struct. Note this not really ideal as numbers in the 15 | * data will lose their precision 16 | */ 17 | google.protobuf.Struct jwtClaims = 2; 18 | } 19 | 20 | service Echo { 21 | rpc echo(EchoRequest) returns (EchoResponse); 22 | rpc echoStream(EchoRequest) returns (stream EchoResponse); 23 | rpc importTest(Import1) returns (google.protobuf.Type); 24 | } 25 | -------------------------------------------------------------------------------- /grpc-service/src/main/proto/import1.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package net.trajano.swarm.sampleservice; 3 | import "import2.proto"; 4 | 5 | message Import1 { 6 | Import2 place = 1000; 7 | } -------------------------------------------------------------------------------- /grpc-service/src/main/proto/import2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package net.trajano.swarm.sampleservice; 3 | 4 | message Import2 { 5 | 6 | } -------------------------------------------------------------------------------- /grpc-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: grpc-service 4 | main: 5 | banner-mode: "off" 6 | web-application-type: none 7 | jmx: 8 | enabled: true 9 | # application: 10 | # admin: 11 | # enabled: true 12 | management: 13 | endpoint: 14 | health: 15 | enabled: true 16 | endpoints: 17 | jmx: 18 | exposure: 19 | include: "*" 20 | -------------------------------------------------------------------------------- /grpc-service/src/test/java/net/trajano/swarm/sampleservice/EchoServiceTest.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.sampleservice; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import com.google.protobuf.Struct; 6 | import com.google.protobuf.util.JsonFormat; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class EchoServiceTest { 10 | 11 | @Test 12 | void echo() throws Exception { 13 | final var jwtClaimsStruct = Struct.newBuilder(); 14 | JsonFormat.parser() 15 | .merge( 16 | " {\n" 17 | + " \"sub\": \"good\",\n" 18 | + " \"exp\": 16643548160,\n" 19 | + " \"iss\": \"http://localhost\",\n" 20 | + " \"jti\": \"3dbecf85-d5f2-2109-5a21-0d135fb35e8a\"\n" 21 | + " }", 22 | jwtClaimsStruct); 23 | System.out.println(jwtClaimsStruct.build()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /jwks-provider/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /jwks-provider/src/main/java/net/trajano/swarm/jwksprovider/Blocks.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.jwksprovider; 2 | 3 | import java.time.Instant; 4 | import java.util.List; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | 8 | @Data 9 | @Builder 10 | public class Blocks { 11 | 12 | @Builder.Default private long currentTimestamp = Instant.now().getEpochSecond(); 13 | 14 | private String previousSigningRedisKey; 15 | 16 | private List previous; 17 | 18 | private String currentSigningRedisKey; 19 | 20 | private List current; 21 | 22 | private String nextSigningRedisKey; 23 | 24 | private List next; 25 | } 26 | -------------------------------------------------------------------------------- /jwks-provider/src/main/java/net/trajano/swarm/jwksprovider/CryptoProvider.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.jwksprovider; 2 | 3 | import java.security.NoSuchAlgorithmException; 4 | import java.security.SecureRandom; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class CryptoProvider { 10 | 11 | @Bean 12 | SecureRandom secureRandom() throws NoSuchAlgorithmException { 13 | 14 | return SecureRandom.getInstanceStrong(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jwks-provider/src/main/java/net/trajano/swarm/jwksprovider/JwkProviderApplication.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.jwksprovider; 2 | 3 | import net.trajano.swarm.gateway.common.AuthProperties; 4 | import net.trajano.swarm.gateway.redis.RedisKeyBlocks; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.scheduling.annotation.EnableScheduling; 8 | 9 | @SpringBootApplication( 10 | scanBasePackageClasses = { 11 | JwkProviderApplication.class, 12 | AuthProperties.class, 13 | RedisKeyBlocks.class 14 | }) 15 | @EnableScheduling 16 | public class JwkProviderApplication { 17 | 18 | public static void main(String[] args) { 19 | SpringApplication.run(JwkProviderApplication.class, args); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /jwks-provider/src/main/java/net/trajano/swarm/jwksprovider/ObservabilityConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.jwksprovider; 2 | 3 | import io.lettuce.core.resource.ClientResources; 4 | import io.micrometer.observation.ObservationRegistry; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.data.redis.connection.lettuce.observability.MicrometerTracingAdapter; 10 | 11 | @Configuration 12 | class ObservabilityConfiguration { 13 | 14 | @Bean 15 | public ClientResources clientResources( 16 | ObservationRegistry observationRegistry, 17 | @Value("${spring.data.redis.host:redis}") final String redisServiceName) { 18 | 19 | return ClientResources.builder() 20 | .tracing(new MicrometerTracingAdapter(observationRegistry, redisServiceName)) 21 | .build(); 22 | } 23 | 24 | @Bean 25 | public LettuceClientConfigurationBuilderCustomizer 26 | observabilityLettuceClientConfigurationBuilderCustomizer(ClientResources clientResources) { 27 | 28 | return clientConfigurationBuilder -> { 29 | clientConfigurationBuilder.clientResources(clientResources); 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /jwks-provider/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: jwks-provider 4 | jmx: 5 | enabled: true 6 | main: 7 | banner-mode: "off" 8 | sleuth: 9 | reactor: 10 | instrumentation-type: decorate_queues 11 | trace-id128: true 12 | logging: 13 | level: 14 | org.springframework.data.repository.config.RepositoryConfigurationDelegate: WARN 15 | management: 16 | health: 17 | redis: 18 | enabled: true 19 | endpoint: 20 | health: 21 | enabled: true 22 | show-details: ALWAYS 23 | endpoints: 24 | enabled-by-default: false 25 | jmx: 26 | exposure: 27 | include: health 28 | web: 29 | exposure: 30 | include: "*" 31 | -------------------------------------------------------------------------------- /logging/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /logging/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'io.spring.dependency-management' version '1.1.0' 3 | id 'net.trajano.swarm.conventions' 4 | id 'java-library' 5 | } 6 | 7 | group = 'net.trajano.swarm' 8 | version = '0.0.1-SNAPSHOT' 9 | 10 | configurations { 11 | compileOnly { 12 | extendsFrom annotationProcessor 13 | } 14 | } 15 | 16 | jar { 17 | enabled = true 18 | } 19 | dependencies { 20 | api 'org.springframework.boot:spring-boot' 21 | runtimeOnly 'net.logstash.logback:logstash-logback-encoder:7.3' 22 | runtimeOnly 'io.micrometer:micrometer-registry-graphite' 23 | runtimeOnly 'io.micrometer:micrometer-registry-cloudwatch2' 24 | runtimeOnly 'io.micrometer:micrometer-registry-prometheus' 25 | runtimeOnly 'io.zipkin.reporter2:zipkin-reporter-brave' 26 | 27 | } 28 | 29 | dependencyManagement { 30 | imports { 31 | mavenBom "org.springframework.boot:spring-boot-dependencies:${springBootVersion}" 32 | } 33 | } 34 | 35 | tasks.named('test') { 36 | useJUnitPlatform() 37 | } 38 | -------------------------------------------------------------------------------- /logging/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.env.EnvironmentPostProcessor=\ 2 | net.trajano.swarm.logging.autoconfig.TraceEnvironmentPostProcessor -------------------------------------------------------------------------------- /logging/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | true 9 | SYSTEM 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@trajano/spring-docker-root", 3 | "version": "1.0.0", 4 | "private": true, 5 | "workspaces": { 6 | "packages": [ 7 | "packages/eslint-config", 8 | "packages/react-hooks", 9 | "packages/*", 10 | "expo-app" 11 | ], 12 | "nohoist": [ 13 | "**/babel*", 14 | "**/eslint", 15 | "**/@typescript-eslint/*", 16 | "**/eslint-*", 17 | "**/jest*", 18 | "**/prettier", 19 | "**/prettier-plugin-*", 20 | "**/webpack*", 21 | "**/typescript", 22 | "typescript", 23 | "**/type-graphql*", 24 | "react-navigation-header-buttons/react-native", 25 | "**/typescript" 26 | ] 27 | }, 28 | "scripts": { 29 | "postinstall": "husky install || true" 30 | }, 31 | "lint-staged": { 32 | "*.json": "prettier --write" 33 | }, 34 | "resolutions": { 35 | "@babel/core": "^7.21.0", 36 | "babel-loader": "^8.3.0", 37 | "react": "18.2.0", 38 | "react-dom": "18.2.0", 39 | "react-native": "0.71.3", 40 | "react-test-renderer": "18.2.0", 41 | "react-native-gesture-handler": "~2.9.0" 42 | }, 43 | "devDependencies": { 44 | "@commitlint/config-conventional": "^17.4.4", 45 | "commitlint": "^17.4.4", 46 | "husky": "^8.0.0", 47 | "lint-staged": "^13.1.2", 48 | "prettier": "^2.8.4", 49 | "prettier-plugin-packagejson": "^2.4.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/auth-context/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /packages/auth-context/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /packages/auth-context/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | 47 | # Ruby 48 | example/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Expo 64 | .expo/* 65 | 66 | # generated by bob 67 | lib/ 68 | dist/ 69 | 70 | # generated by bob 71 | lib/ 72 | -------------------------------------------------------------------------------- /packages/auth-context/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /packages/auth-context/.yarnrc: -------------------------------------------------------------------------------- 1 | # Override Yarn command so we can automatically setup the repo on running `yarn` 2 | 3 | yarn-path "scripts/bootstrap.js" 4 | -------------------------------------------------------------------------------- /packages/auth-context/README.md: -------------------------------------------------------------------------------- 1 | # @trajano/spring-docker-auth-context 2 | 3 | Auth Context 4 | 5 | ## License 6 | 7 | EPL-2.0 8 | -------------------------------------------------------------------------------- /packages/auth-context/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/auth-context/example/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /packages/auth-context/example/App.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/App'; 2 | -------------------------------------------------------------------------------- /packages/auth-context/example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/adaptive-icon.png", 26 | "backgroundColor": "#FFFFFF" 27 | } 28 | }, 29 | "web": { 30 | "favicon": "./assets/favicon.png" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/auth-context/example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/packages/auth-context/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /packages/auth-context/example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/packages/auth-context/example/assets/favicon.png -------------------------------------------------------------------------------- /packages/auth-context/example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/packages/auth-context/example/assets/icon.png -------------------------------------------------------------------------------- /packages/auth-context/example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/packages/auth-context/example/assets/splash.png -------------------------------------------------------------------------------- /packages/auth-context/example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = function (api) { 5 | api.cache(true); 6 | 7 | return { 8 | presets: ['babel-preset-expo'], 9 | plugins: [ 10 | [ 11 | 'module-resolver', 12 | { 13 | extensions: ['.tsx', '.ts', '.js', '.json'], 14 | alias: { 15 | // For development, we want to alias the library to the source 16 | [pak.name]: path.join(__dirname, '..', pak.source), 17 | }, 18 | }, 19 | ], 20 | ], 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/auth-context/example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const escape = require('escape-string-regexp'); 3 | const { getDefaultConfig } = require('@expo/metro-config'); 4 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 5 | const pak = require('../package.json'); 6 | 7 | const root = path.resolve(__dirname, '..'); 8 | 9 | const modules = Object.keys({ 10 | ...pak.peerDependencies, 11 | }); 12 | 13 | const defaultConfig = getDefaultConfig(__dirname); 14 | 15 | module.exports = { 16 | ...defaultConfig, 17 | 18 | projectRoot: __dirname, 19 | watchFolders: [root], 20 | 21 | // We need to make sure that only one version is loaded for peerDependencies 22 | // So we block them at the root, and alias them to the versions in example's node_modules 23 | resolver: { 24 | ...defaultConfig.resolver, 25 | 26 | blacklistRE: exclusionList( 27 | modules.map( 28 | (m) => 29 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 30 | ) 31 | ), 32 | 33 | extraNodeModules: modules.reduce((acc, name) => { 34 | acc[name] = path.join(__dirname, 'node_modules', name); 35 | return acc; 36 | }, {}), 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/auth-context/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "expo": "~46.0.16", 13 | "expo-status-bar": "~1.4.0", 14 | "react": "18.0.0", 15 | "react-native": "0.69.6" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.12.9", 19 | "babel-plugin-module-resolver": "^4.1.0" 20 | }, 21 | "private": true 22 | } -------------------------------------------------------------------------------- /packages/auth-context/example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { AuthState } from '@trajano/spring-docker-auth-context'; 2 | import * as React from 'react'; 3 | import { StyleSheet, Text, View } from 'react-native'; 4 | 5 | export default function App() { 6 | const [result, setResult] = React.useState(); 7 | 8 | React.useEffect(() => { 9 | (async () => { 10 | const nextResult = await Promise.resolve(AuthState.AUTHENTICATED); 11 | setResult(nextResult); 12 | })(); 13 | }, []); 14 | 15 | return ( 16 | 17 | Result: {result} 18 | 19 | ); 20 | } 21 | 22 | const styles = StyleSheet.create({ 23 | container: { 24 | flex: 1, 25 | alignItems: 'center', 26 | justifyContent: 'center', 27 | }, 28 | box: { 29 | width: 60, 30 | height: 60, 31 | marginVertical: 20, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/auth-context/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | // Avoid expo-cli auto-generating a tsconfig 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/auth-context/example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config'); 3 | const { resolver } = require('./metro.config'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const node_modules = path.join(__dirname, 'node_modules'); 7 | 8 | module.exports = async function (env, argv) { 9 | const config = await createExpoWebpackConfigAsync(env, argv); 10 | 11 | config.module.rules.push({ 12 | test: /\.(js|jsx|ts|tsx)$/, 13 | include: path.resolve(root, 'src'), 14 | use: 'babel-loader', 15 | }); 16 | 17 | // We need to make sure that only one version is loaded for peerDependencies 18 | // So we alias them to the versions in example's node_modules 19 | Object.assign(config.resolve.alias, { 20 | ...resolver.extraNodeModules, 21 | 'react-native-web': path.join(node_modules, 'react-native-web'), 22 | }); 23 | 24 | return config; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/auth-context/scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const child_process = require('child_process'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const args = process.argv.slice(2); 7 | const options = { 8 | cwd: process.cwd(), 9 | env: process.env, 10 | stdio: 'inherit', 11 | encoding: 'utf-8', 12 | }; 13 | 14 | if (os.type() === 'Windows_NT') { 15 | options.shell = true; 16 | } 17 | 18 | let result; 19 | 20 | if (process.cwd() !== root || args.length) { 21 | // We're not in the root of the project, or additional arguments were passed 22 | // In this case, forward the command to `yarn` 23 | result = child_process.spawnSync('yarn', args, options); 24 | } else { 25 | // If `yarn` is run without arguments, perform bootstrap 26 | result = child_process.spawnSync('yarn', ['bootstrap'], options); 27 | } 28 | 29 | process.exitCode = result.status; 30 | -------------------------------------------------------------------------------- /packages/auth-context/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import noop from 'lodash/noop'; 2 | import { createContext } from 'react'; 3 | 4 | import { AuthState } from './AuthState'; 5 | import type { IAuth } from './IAuth'; 6 | import { buildSimpleEndpointConfiguration } from './buildSimpleEndpointConfiguration'; 7 | 8 | export const AuthContext = createContext>({ 9 | accessToken: null, 10 | accessTokenExpired: true, 11 | appDataLoaded: false, 12 | authorization: null, 13 | authState: AuthState.INITIAL, 14 | backendReachable: false, 15 | baseUrl: 'https://undefined/', 16 | endpointConfiguration: buildSimpleEndpointConfiguration('https://undefined/'), 17 | forceCheckAuthStorageAsync: () => Promise.resolve(), 18 | lastCheckAt: new Date(), 19 | loginAsync: () => Promise.reject(new Error()), 20 | logoutAsync: () => Promise.resolve(), 21 | oauthToken: null, 22 | refreshAsync: () => Promise.reject(new Error()), 23 | setEndpointConfiguration: noop, 24 | signalAppDataLoaded: noop, 25 | signalStart: noop, 26 | signalTokenProcessed: noop, 27 | subscribe: () => noop, 28 | tokenExpiresAt: new Date(0), 29 | }); 30 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthProvider } from './AuthProvider'; 2 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/isTokenExpired.test.ts: -------------------------------------------------------------------------------- 1 | import { isTokenExpired } from './isTokenExpired'; 2 | beforeEach(() => { 3 | jest.useFakeTimers({ advanceTimers: true }); 4 | }); 5 | afterEach(() => { 6 | jest.useRealTimers(); 7 | }); 8 | it('Expired on the spot', () => { 9 | jest.setSystemTime(new Date('2022-01-01T00:00:00Z')); 10 | expect(isTokenExpired(new Date('2022-01-01T00:00:00Z'), 10000)).toBeTruthy(); 11 | }); 12 | 13 | it('Expired in window', () => { 14 | jest.setSystemTime(new Date('2022-01-01T00:00:00Z')); 15 | expect(isTokenExpired(new Date('2022-01-01T00:00:10Z'), 10000)).toBeTruthy(); 16 | }); 17 | 18 | it('Expired in window with advancing timers', () => { 19 | jest.setSystemTime(new Date('2022-01-01T00:00:00Z')); 20 | 21 | const expiresAt = new Date('2022-01-01T00:00:20Z'); 22 | expect(isTokenExpired(expiresAt, 10000)).toBeFalsy(); 23 | jest.advanceTimersByTime(9000); 24 | expect(isTokenExpired(expiresAt, 10000)).toBeFalsy(); 25 | jest.advanceTimersByTime(999); 26 | expect(isTokenExpired(expiresAt, 10000)).toBeFalsy(); 27 | jest.advanceTimersByTime(1); 28 | expect(isTokenExpired(expiresAt, 10000)).toBeTruthy(); 29 | }); 30 | 31 | it('Expired when null', () => { 32 | jest.setSystemTime(new Date('2022-01-01T00:00:00Z')); 33 | 34 | expect(isTokenExpired(null, 10000)).toBeTruthy(); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/isTokenExpired.ts: -------------------------------------------------------------------------------- 1 | import { isBefore, subMilliseconds } from 'date-fns'; 2 | 3 | export function isTokenExpired( 4 | tokenExpiresAt: Date | null | undefined, 5 | timeBeforeExpirationRefresh: number 6 | ): boolean { 7 | return ( 8 | !tokenExpiresAt || 9 | !isBefore( 10 | Date.now(), 11 | subMilliseconds(tokenExpiresAt, timeBeforeExpirationRefresh) 12 | ) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/timeToNextExpirationCheck.test.ts: -------------------------------------------------------------------------------- 1 | import { timeToNextExpirationCheck } from './timeToNextExpirationCheck'; 2 | 3 | beforeEach(() => jest.useFakeTimers()); 4 | afterAll(() => jest.useRealTimers()); 5 | it('should handle future time', () => { 6 | expect( 7 | timeToNextExpirationCheck(new Date(Date.now() + 2000), 100, 60000) 8 | ).toBe(1900); 9 | }); 10 | 11 | it('should cap to max', () => { 12 | expect( 13 | timeToNextExpirationCheck(new Date(Date.now() + 200000), 100, 60000) 14 | ).toBe(60000); 15 | }); 16 | 17 | it('should handle edge case of zero due to time before expiration refresh', () => { 18 | expect( 19 | timeToNextExpirationCheck(new Date(Date.now() + 200), 200, 60000) 20 | ).toBe(0); 21 | }); 22 | 23 | it('should handle edge case of zero with zero time before expiration refresh', () => { 24 | expect(timeToNextExpirationCheck(new Date(), 0, 60000)).toBe(0); 25 | }); 26 | 27 | it('should handle edge case of -1 with zero time before expiration refresh', () => { 28 | expect(timeToNextExpirationCheck(new Date(Date.now() - 1), 0, 60000)).toBe(0); 29 | }); 30 | 31 | it('should case where it is expire past the limit with time before expiration refresh', () => { 32 | expect( 33 | timeToNextExpirationCheck(new Date(Date.now() + 200), 500, 60000) 34 | ).toBe(0); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/timeToNextExpirationCheck.ts: -------------------------------------------------------------------------------- 1 | import { differenceInMilliseconds, subMilliseconds } from 'date-fns'; 2 | 3 | /** 4 | * Milliseconds till the token needs to be checked for expiration again. This is 5 | * capped by `maxTimeoutForRefreshCheck`. 6 | * 7 | * @param tokenExpiresAt When does the token actual expire 8 | * @param timeBeforeExpirationRefreshMs The amount of ms to subtract from the 9 | * expiration time so it can be refreshed sooner. 10 | * @param maxTimeoutForRefreshCheckMs Maximum number of ms before the check is 11 | * fired again. Should not be larger than 60 seconds. 12 | * @returns Time before expiration. This will never be negative, but may be 13 | * zero. 14 | */ 15 | export const timeToNextExpirationCheck = ( 16 | tokenExpiresAt: Date, 17 | timeBeforeExpirationRefreshMs: number, 18 | maxTimeoutForRefreshCheckMs: number 19 | ) => 20 | Math.max( 21 | 0, 22 | Math.min( 23 | differenceInMilliseconds( 24 | subMilliseconds(tokenExpiresAt, timeBeforeExpirationRefreshMs), 25 | Date.now() 26 | ), 27 | maxTimeoutForRefreshCheckMs 28 | ) 29 | ); 30 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/useAppStateWithNetInfoRefresh.ts: -------------------------------------------------------------------------------- 1 | import NetInfo from '@react-native-community/netinfo'; 2 | import { useEffect, useState } from 'react'; 3 | import { AppState, AppStateStatus } from 'react-native'; 4 | 5 | export function useAppStateWithNetInfoRefresh(): AppStateStatus { 6 | /** App State */ 7 | const [appState, setAppState] = useState( 8 | AppState.currentState 9 | ); 10 | useEffect( 11 | /** Monitors app state changes. */ 12 | () => { 13 | const appStateSubscription = AppState.addEventListener( 14 | 'change', 15 | (nextAppState) => { 16 | if (nextAppState === 'active') { 17 | // if the app switches to active, force a NetInfo refresh 18 | NetInfo.refresh().catch(console.error); 19 | } 20 | setAppState(nextAppState); 21 | } 22 | ); 23 | return () => { 24 | appStateSubscription.remove(); 25 | }; 26 | }, 27 | [] 28 | ); 29 | return appState; 30 | } 31 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/useNoTokenAvailableEffect/index.ts: -------------------------------------------------------------------------------- 1 | /** Handles states when the token is not available. */ 2 | export { useNoTokenAvailableEffect } from './useNoTokenAvailableEffect'; 3 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/useNoTokenAvailableEffect/useNoTokenAvailableEffect.ts: -------------------------------------------------------------------------------- 1 | import { useUnauthenticatedOfflineStateEffect } from './useUnauthenticatedOfflineStateEffect'; 2 | import { useUnauthenicatedStateEffect } from './useUnauthenticatedStateEffect'; 3 | import type { InternalProviderState } from '../InternalProviderState'; 4 | 5 | export const useNoTokenAvailableEffect = ( 6 | providerState: InternalProviderState 7 | ) => { 8 | useUnauthenicatedStateEffect(providerState); 9 | useUnauthenticatedOfflineStateEffect(providerState); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/useNoTokenAvailableEffect/useUnauthenticatedOfflineStateEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | import { AuthState } from '../../AuthState'; 4 | import type { InternalProviderState } from '../InternalProviderState'; 5 | export const useUnauthenticatedOfflineStateEffect = ({ 6 | authState, 7 | backendReachable, 8 | setAuthState, 9 | }: InternalProviderState) => { 10 | useEffect(() => { 11 | if (authState !== AuthState.UNAUTHENTICATED_OFFLINE) { 12 | return; 13 | } 14 | if (backendReachable) { 15 | setAuthState(AuthState.UNAUTHENTICATED); 16 | } 17 | }, [authState, backendReachable, setAuthState]); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/useNoTokenAvailableEffect/useUnauthenticatedStateEffect.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, useEffect } from 'react'; 2 | 3 | import { AuthState } from '../../AuthState'; 4 | 5 | export const useUnauthenicatedStateEffect = ({ 6 | authState, 7 | backendReachable, 8 | setAuthState, 9 | }: { 10 | authState: AuthState; 11 | backendReachable: boolean; 12 | setAuthState: Dispatch; 13 | }) => { 14 | useEffect(() => { 15 | if (authState !== AuthState.UNAUTHENTICATED) { 16 | return; 17 | } 18 | if (!backendReachable) { 19 | setAuthState(AuthState.UNAUTHENTICATED_OFFLINE); 20 | } 21 | }, [authState, backendReachable, setAuthState]); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/useTokenAvailableEffect/index.ts: -------------------------------------------------------------------------------- 1 | /** Handles states when the token is available. */ 2 | export { useTokenAvailableEffect } from './useTokenAvailableEffect'; 3 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/useTokenAvailableEffect/useBackendFailureStateEffect.ts: -------------------------------------------------------------------------------- 1 | import noop from 'lodash/noop'; 2 | import { useEffect } from 'react'; 3 | 4 | import { AuthState } from '../../AuthState'; 5 | import type { InternalProviderState } from '../InternalProviderState'; 6 | 7 | export const useBackendFailureStateEffect = ({ 8 | authState, 9 | notify, 10 | setAuthState, 11 | }: InternalProviderState) => { 12 | useEffect(() => { 13 | if (authState !== AuthState.BACKEND_FAILURE) { 14 | return noop; 15 | } 16 | notify({ 17 | authState, 18 | reason: 'timeout for backend failure retry set', 19 | type: 'CheckRefresh', 20 | }); 21 | const timeoutID = setTimeout(() => { 22 | notify({ 23 | authState, 24 | reason: 25 | 'timeout for backend failure retry switching back to NeedsRefresh', 26 | type: 'CheckRefresh', 27 | }); 28 | setAuthState(AuthState.DISPATCHING); 29 | }, 2000); 30 | return () => clearTimeout(timeoutID); 31 | }, [authState, notify, setAuthState]); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/useTokenAvailableEffect/useBackendInaccessibleStateEffect.ts: -------------------------------------------------------------------------------- 1 | import noop from 'lodash/noop'; 2 | import { useEffect } from 'react'; 3 | 4 | import { AuthState } from '../../AuthState'; 5 | import type { InternalProviderState } from '../InternalProviderState'; 6 | 7 | export const useBackendInaccessibleStateEffect = ({ 8 | authState, 9 | backendReachable, 10 | notify, 11 | setAuthState, 12 | }: Pick< 13 | InternalProviderState, 14 | 'authState' | 'backendReachable' | 'notify' | 'setAuthState' 15 | >) => { 16 | useEffect(() => { 17 | if (authState !== AuthState.BACKEND_INACCESSIBLE) { 18 | return noop; 19 | } 20 | if (backendReachable) { 21 | notify({ 22 | type: 'PingSucceeded', 23 | authState, 24 | reason: `Backend was determined to be not reachable while authenticated`, 25 | backendReachable, 26 | }); 27 | 28 | setAuthState(AuthState.DISPATCHING); 29 | return noop; 30 | } 31 | return noop; 32 | }, [authState, backendReachable, notify, setAuthState]); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/useTokenAvailableEffect/useTokenAvailableEffect.ts: -------------------------------------------------------------------------------- 1 | import { useAuthenticatedStateEffect } from './useAuthenticatedStateEffect'; 2 | import { useBackendFailureStateEffect } from './useBackendFailureStateEffect'; 3 | import { useBackendInaccessibleStateEffect } from './useBackendInaccessibleStateEffect'; 4 | import { useDispatchingStateEffect } from './useDispatchingStateEffect'; 5 | import { useRefreshingStateEffect } from './useRefreshingStateEffect'; 6 | import { useRestoringStateEffect } from './useRestoringStateEffect'; 7 | import { useTokenRemovalState } from './useTokenRemovalState'; 8 | import { useUsableTokenStateEffect } from './useUsableTokenStateEffect'; 9 | import type { InternalProviderState } from '../InternalProviderState'; 10 | 11 | export const useTokenAvailableEffect = ( 12 | internalProviderState: InternalProviderState 13 | ) => { 14 | useAuthenticatedStateEffect(internalProviderState); 15 | useBackendFailureStateEffect(internalProviderState); 16 | useBackendInaccessibleStateEffect(internalProviderState); 17 | useDispatchingStateEffect(internalProviderState); 18 | useRefreshingStateEffect(internalProviderState); 19 | useRestoringStateEffect(internalProviderState); 20 | useTokenRemovalState(internalProviderState); 21 | useUsableTokenStateEffect(internalProviderState); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthProvider/useTokenAvailableEffect/useTokenRemovalState.ts: -------------------------------------------------------------------------------- 1 | import noop from 'lodash/noop'; 2 | import { useEffect } from 'react'; 3 | 4 | import { AuthState } from '../../AuthState'; 5 | import type { InternalProviderState } from '../InternalProviderState'; 6 | 7 | export const useTokenRemovalState = ({ 8 | authStorage, 9 | authState, 10 | signaled, 11 | notify, 12 | setAuthState, 13 | resetAppDataLoaded, 14 | resetTokenProcessed, 15 | setTokenExpiresAt, 16 | setOAuthToken, 17 | }: InternalProviderState) => { 18 | useEffect(() => { 19 | if (!signaled || authState !== AuthState.TOKEN_REMOVAL) { 20 | return noop; 21 | } 22 | (async () => { 23 | await authStorage.clearAsync(); 24 | notify({ 25 | type: 'Unauthenticated', 26 | authState, 27 | reason: 'Token removed', 28 | }); 29 | setOAuthToken(null); 30 | resetAppDataLoaded(); 31 | resetTokenProcessed(); 32 | setTokenExpiresAt(new Date(0)); 33 | setAuthState(AuthState.UNAUTHENTICATED); 34 | })(); 35 | return noop; 36 | }, [ 37 | authState, 38 | authStorage, 39 | signaled, 40 | notify, 41 | resetAppDataLoaded, 42 | resetTokenProcessed, 43 | setAuthState, 44 | setOAuthToken, 45 | setTokenExpiresAt, 46 | ]); 47 | }; 48 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthStore/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthStore } from './AuthStore'; 2 | export type { IAuthStore } from './IAuthStore'; 3 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthStore/isValidOAuthToken.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /** 3 | * Ensures that the value is a valid OAuth token. 4 | * 5 | * @param oauthToken Object to check 6 | * @returns True if valid. 7 | */ 8 | export function isValidOAuthToken(oauthToken?: any): boolean { 9 | return ( 10 | !!oauthToken && 11 | typeof oauthToken === 'object' && 12 | typeof oauthToken.access_token === 'string' && 13 | oauthToken.access_token !== '' && 14 | typeof oauthToken.refresh_token === 'string' && 15 | oauthToken.refresh_token !== '' && 16 | typeof oauthToken.expires_in === 'number' && 17 | oauthToken.token_type === 'Bearer' 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/auth-context/src/AuthenticationClientError.ts: -------------------------------------------------------------------------------- 1 | export class AuthenticationClientError extends Error { 2 | public readonly isAuthenticationClientError = true; 3 | constructor( 4 | public response: Response, 5 | public responseBody: string, 6 | message?: string 7 | ) { 8 | super(message ?? `HTTP Error ${response.status}`); 9 | } 10 | /** 11 | * Explicit check for 401 errors. 12 | * 13 | * @returns If status code is 401 14 | */ 15 | public isUnauthorized(): boolean { 16 | return this.response.status === 401; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/auth-context/src/EndpointConfiguration.ts: -------------------------------------------------------------------------------- 1 | /** Endpoint configuration. Note these are `string` to allow JSON serialization. */ 2 | export interface EndpointConfiguration { 3 | /** This is the base URL for operations to use. */ 4 | baseUrl: string; 5 | /** 6 | * The endpoint that receives the credential data to provide the initial OAuth 7 | * token. 8 | */ 9 | authorizationEndpoint: string; 10 | /** Endpoint that will be called to refresh the access token. */ 11 | refreshEndpoint: string; 12 | /** Endpoint that will be called to revoke the refresh token. */ 13 | revocationEndpoint: string; 14 | /** 15 | * Endpoint that will be called with a HEAD request to check if the backend is 16 | * alive or not. 17 | */ 18 | pingEndpoint: string; 19 | /** Client ID. */ 20 | clientId: string; 21 | /** Client secret. */ 22 | clientSecret: string; 23 | } 24 | -------------------------------------------------------------------------------- /packages/auth-context/src/OAuthToken.ts: -------------------------------------------------------------------------------- 1 | export interface OAuthToken { 2 | [key: string]: unknown; 3 | access_token: string; 4 | refresh_token: string; 5 | expires_in: number; 6 | token_type: 'Bearer'; 7 | } 8 | -------------------------------------------------------------------------------- /packages/auth-context/src/__mocks__/@react-native-async-storage/async-storage.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock'; 2 | export default AsyncStorage; 3 | -------------------------------------------------------------------------------- /packages/auth-context/src/__mocks__/react-native/Libraries/Animated/NativeAnimatedHelper.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/packages/auth-context/src/__mocks__/react-native/Libraries/Animated/NativeAnimatedHelper.ts -------------------------------------------------------------------------------- /packages/auth-context/src/__tests__/__snapshots__/testing-library.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`examples of some things 1`] = ` 4 | 5 | 10 | 41 | 42 | Print Username 43 | 44 | 45 | 48 | Ada Lovelace 49 | 50 | 51 | `; 52 | -------------------------------------------------------------------------------- /packages/auth-context/src/basicAuthorization.test.ts: -------------------------------------------------------------------------------- 1 | import { basicAuthorization } from './basicAuthorization'; 2 | it('simple example', () => { 3 | expect(basicAuthorization('simple', 'example')).toBe( 4 | 'Basic c2ltcGxlOmV4YW1wbGU=' 5 | ); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/auth-context/src/basicAuthorization.ts: -------------------------------------------------------------------------------- 1 | import { encode } from 'js-base64'; 2 | 3 | export function basicAuthorization(username: string, password: string): string { 4 | const encoded = encode(`${username}:${password}`); 5 | return `Basic ${encoded}`; 6 | } 7 | -------------------------------------------------------------------------------- /packages/auth-context/src/buildSimpleEndpointConfiguration.test.ts: -------------------------------------------------------------------------------- 1 | import { buildSimpleEndpointConfiguration } from './buildSimpleEndpointConfiguration'; 2 | it('simple example', () => { 3 | expect( 4 | buildSimpleEndpointConfiguration( 5 | 'https://api.trajano.net/', 6 | 'simple', 7 | 'example' 8 | ) 9 | ).toEqual({ 10 | baseUrl: 'https://api.trajano.net/', 11 | authorizationEndpoint: 'https://api.trajano.net/auth', 12 | refreshEndpoint: 'https://api.trajano.net/refresh', 13 | pingEndpoint: 'https://api.trajano.net/ping', 14 | revocationEndpoint: 'https://api.trajano.net/logout', 15 | clientId: 'simple', 16 | clientSecret: 'example', 17 | }); 18 | }); 19 | it('should warn if the trailing slash is missing', () => { 20 | expect(() => { 21 | buildSimpleEndpointConfiguration( 22 | 'https://api.trajano.net', 23 | 'simple', 24 | 'example' 25 | ); 26 | }).toThrow( 27 | new Error("baseUrl=https://api.trajano.net should end with a '/'") 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/auth-context/src/buildSimpleEndpointConfiguration.ts: -------------------------------------------------------------------------------- 1 | import type { EndpointConfiguration } from './EndpointConfiguration'; 2 | 3 | /** 4 | * This builds a simple endpoint configuration that is used by Spring Docker 5 | * project. It does the conversion to URL to ensure that the input is valid. 6 | * 7 | * @param inBaseUrl Base URL must have trailing slash. 8 | */ 9 | export function buildSimpleEndpointConfiguration( 10 | baseUrl: string, 11 | clientId = 'unknown', 12 | clientSecret = 'unknown' 13 | ): EndpointConfiguration { 14 | /* istanbul ignore next */ 15 | if (__DEV__) { 16 | if (!baseUrl.endsWith('/')) { 17 | throw new Error(`baseUrl=${baseUrl} should end with a '/'`); 18 | } 19 | } 20 | return { 21 | baseUrl, 22 | authorizationEndpoint: `${baseUrl}auth`, 23 | refreshEndpoint: `${baseUrl}refresh`, 24 | revocationEndpoint: `${baseUrl}logout`, 25 | pingEndpoint: `${baseUrl}ping`, 26 | clientId, 27 | clientSecret, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/auth-context/src/index.test.tsx: -------------------------------------------------------------------------------- 1 | describe('ExampleComponent', () => { 2 | it('is truthy', () => { 3 | expect(true).toBeTruthy(); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/auth-context/src/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthenticationClientError } from './AuthenticationClientError'; 2 | export type { AuthEvent } from './AuthEvent'; 3 | export { AuthProvider } from './AuthProvider'; 4 | export { AuthState } from './AuthState'; 5 | export { AuthStore } from './AuthStore'; 6 | export type { IAuthStore } from './AuthStore'; 7 | export { buildSimpleEndpointConfiguration } from './buildSimpleEndpointConfiguration'; 8 | export type { EndpointConfiguration } from './EndpointConfiguration'; 9 | export type { IAuth } from './IAuth'; 10 | export { useAuth } from './useAuth'; 11 | export { validateEndpointConfiguration } from './validateEndpointConfiguration'; 12 | export type { OAuthToken } from './OAuthToken'; 13 | -------------------------------------------------------------------------------- /packages/auth-context/src/useAppActiveState.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { AppState } from 'react-native'; 3 | 4 | export const useAppActiveState = (): boolean => { 5 | const [appActiveState, setAppActiveState] = useState( 6 | AppState.currentState === 'active' 7 | ); 8 | useEffect(() => { 9 | const appStateSubscription = AppState.addEventListener( 10 | 'change', 11 | (nextAppState) => { 12 | setAppActiveState(nextAppState === 'active'); 13 | } 14 | ); 15 | return () => appStateSubscription.remove(); 16 | }, []); 17 | return appActiveState; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/auth-context/src/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import { AuthContext } from './AuthContext'; 4 | import type { IAuth } from './IAuth'; 5 | 6 | export function useAuth(): IAuth { 7 | return useContext(AuthContext); 8 | } 9 | -------------------------------------------------------------------------------- /packages/auth-context/src/useBackendReachable.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react-native'; 2 | import fetchMock from 'fetch-mock'; 3 | 4 | import { buildSimpleEndpointConfiguration } from './buildSimpleEndpointConfiguration'; 5 | import { useBackendReachable } from './useBackendReachable'; 6 | it('should say it is reachable if the ping endpoint is up after effect', async () => { 7 | fetchMock.get('https://asdf.com/ping', { body: { ok: true } }); 8 | 9 | const { result } = renderHook(() => 10 | useBackendReachable(buildSimpleEndpointConfiguration('https://asdf.com/')) 11 | ); 12 | expect(result.current).toBe(false); 13 | await act(() => Promise.resolve()); 14 | expect(result.current).toBe(true); 15 | }); 16 | afterEach(() => fetchMock.reset()); 17 | -------------------------------------------------------------------------------- /packages/auth-context/src/useBackendReachable.ts: -------------------------------------------------------------------------------- 1 | import NetInfo from '@react-native-community/netinfo'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import type { EndpointConfiguration } from './EndpointConfiguration'; 5 | 6 | export const useBackendReachable = ( 7 | endpointConfiguration: EndpointConfiguration 8 | ): boolean => { 9 | const [backendReachable, setBackendReachable] = useState(false); 10 | useEffect(() => { 11 | NetInfo.configure({ 12 | reachabilityUrl: endpointConfiguration.pingEndpoint, 13 | reachabilityTest: (response) => 14 | Promise.resolve(response.status === 200 || response.status === 204), 15 | useNativeReachability: true, 16 | }); 17 | 18 | (async () => { 19 | const nextStatus = await NetInfo.refresh(); 20 | setBackendReachable(nextStatus.isInternetReachable === true); 21 | })(); 22 | 23 | return NetInfo.addEventListener((nextStatus) => { 24 | setBackendReachable(nextStatus.isInternetReachable === true); 25 | }); 26 | }, [endpointConfiguration.pingEndpoint]); 27 | return backendReachable; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/auth-context/src/useNetInfoState/index.ts: -------------------------------------------------------------------------------- 1 | export { useNetInfoState } from './useNetInfoState'; 2 | -------------------------------------------------------------------------------- /packages/auth-context/src/useNetInfoState/netInfoStateReducer.ts: -------------------------------------------------------------------------------- 1 | import type { NetInfoState } from '@react-native-community/netinfo'; 2 | 3 | /** 4 | * Since only the values of type (when switching from mobile to wifi which may 5 | * change IPs), isConnected (connection) and isInternetReachable (connection to 6 | * server) are relevant. Only those should trigger a state change. 7 | */ 8 | export function netInfoStateReducer( 9 | prev: NetInfoState, 10 | next: NetInfoState 11 | ): NetInfoState { 12 | if ( 13 | prev.type === next.type && 14 | prev.isConnected === next.isConnected && 15 | prev.isInternetReachable === next.isInternetReachable 16 | ) { 17 | return prev; 18 | } else { 19 | return next; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/auth-context/src/useNetInfoState/useNetInfoState.test.ts: -------------------------------------------------------------------------------- 1 | import { act, cleanup, renderHook } from '@testing-library/react-native'; 2 | 3 | import { useNetInfoState } from './useNetInfoState'; 4 | afterEach(cleanup); 5 | beforeEach(() => { 6 | jest.useFakeTimers({ advanceTimers: true }); 7 | }); 8 | 9 | it('should switch to connected', async () => { 10 | const { result } = renderHook(() => 11 | useNetInfoState({ pingEndpoint: 'https://api.trajano.net/ping' }) 12 | ); 13 | // initial value 14 | expect(result.current).toStrictEqual({ 15 | type: 'unknown', 16 | isConnected: null, 17 | isInternetReachable: null, 18 | details: null, 19 | }); 20 | await act(() => Promise.resolve()); 21 | expect(result.current.isConnected).toBeTruthy(); 22 | }); 23 | afterEach(() => { 24 | jest.useRealTimers(); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/auth-context/src/validateEndpointConfiguration.ts: -------------------------------------------------------------------------------- 1 | import type { EndpointConfiguration } from './EndpointConfiguration'; 2 | 3 | export function validateEndpointConfiguration( 4 | endpointConfiguration: EndpointConfiguration 5 | ) { 6 | if (!endpointConfiguration.baseUrl.endsWith('/')) { 7 | throw new Error( 8 | `baseUrl=${endpointConfiguration.baseUrl} should end with a '/'` 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/auth-context/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": [ 4 | "example", 5 | "**/*.test.ts", 6 | "**/*.test.tsx", 7 | "**/__tests__", 8 | "**/__mocks__" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/auth-context/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@trajano/spring-docker-auth-context": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "allowJs": false, 11 | "importsNotUsedAsValues": "error", 12 | "forceConsistentCasingInFileNames": true, 13 | "jsx": "react", 14 | "lib": ["esnext"], 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitReturns": true, 19 | "noImplicitUseStrict": false, 20 | "noStrictGenericChecks": false, 21 | "noUncheckedIndexedAccess": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "resolveJsonModule": true, 25 | "skipLibCheck": true, 26 | "strict": true, 27 | "target": "esnext" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # @trajano/eslint-config 2 | 3 | Single place where all the ESLint configuration is performed. 4 | 5 | Prettier is defined here but there is no configuration, the intent is to let prettier run as it's defaults as much as possible. 6 | 7 | ## Usage 8 | 9 | In package.json 10 | 11 | ```json 12 | "eslintConfig": { 13 | "extends": [ 14 | "@trajano" 15 | ] 16 | }, 17 | ``` 18 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@trajano/eslint-config", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "EPL-2.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "lint": "eslint index.js", 9 | "prepare": "eslint index.js", 10 | "test": "eslint index.js" 11 | }, 12 | "lint-staged": { 13 | "*.js": "eslint --fix", 14 | "*.{json,md}": "prettier --check" 15 | }, 16 | "eslintConfig": { 17 | "env": { 18 | "node": true 19 | }, 20 | "plugins": [ 21 | "prettier" 22 | ], 23 | "extends": [ 24 | "eslint:recommended", 25 | "prettier" 26 | ], 27 | "rules": { 28 | "prettier/prettier": "error" 29 | }, 30 | "root": true 31 | }, 32 | "dependencies": { 33 | "eslint": "^8.35.0", 34 | "eslint-config-universe": "^11.1.1", 35 | "eslint-import-resolver-typescript": "^3.5.3", 36 | "eslint-plugin-jest": "^27.2.1", 37 | "eslint-plugin-node": "^11.1.0", 38 | "eslint-plugin-testing-library": "^5.10.2", 39 | "prettier": "^2.8.4", 40 | "prettier-plugin-jsdoc": "^0.4.2", 41 | "prettier-plugin-packagejson": "^2.4.3", 42 | "typescript": "^4.9.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /react-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@trajano/spring-docker-react-app", 3 | "version": "0.1.0", 4 | "main": "dist/trajano-spring-docker-react-app.cjs.js", 5 | "module": "dist/trajano-spring-docker-react-app.esm.js", 6 | "private": true, 7 | "dependencies": { 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "@types/jest": "^27.5.2", 12 | "@types/node": "^16.18.2", 13 | "@types/react": "^18.0.24", 14 | "@types/react-dom": "^18.0.8", 15 | "google-protobuf": "^3.21.2", 16 | "grpc-web": "^1.4.2", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-scripts": "5.0.1", 20 | "web-vitals": "^2.1.4" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "typescript": "4.8.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/react-app/public/favicon.ico -------------------------------------------------------------------------------- /react-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/react-app/public/logo192.png -------------------------------------------------------------------------------- /react-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trajano/spring-cloud-demo/78ad12e7db4198fbedd94d2b2a4e87f5ae5ff187/react-app/public/logo512.png -------------------------------------------------------------------------------- /react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /react-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /react-app/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /react-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |

8 |
9 | logo 10 |

11 | Edit src/App.tsx and save to reload. 12 |

13 | 19 | Learn React 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /react-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /react-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /react-app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /react-app/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /react-app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /react-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /redocly.yaml: -------------------------------------------------------------------------------- 1 | apis: 2 | spring-cloud-docker-swarm: 3 | root: ./src/openapi/spring-cloud-docker-swarm.yaml 4 | -------------------------------------------------------------------------------- /reyarn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -x 4 | git submodule update --init --recursive 5 | git clean -fdx expo-app packages/* 6 | pushd packages/react-hooks 7 | git clean -fdx . 8 | popd 9 | # rm -f package-lock.json 10 | # pnpm i 11 | # npm run prepare --workspaces 12 | # npm run prepare 13 | yarn 14 | yarn workspaces run prepare 15 | 16 | -------------------------------------------------------------------------------- /sample-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /sample-service/src/main/java/net/trajano/swarm/sampleservice/CounterProvider.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.sampleservice; 2 | 3 | import io.micrometer.core.instrument.Counter; 4 | import io.micrometer.core.instrument.MeterRegistry; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class CounterProvider { 10 | 11 | @Bean 12 | Counter successfulApiRequests(MeterRegistry meterRegistry) { 13 | 14 | return Counter.builder("sample.api.calls.success") 15 | .description("Successful API calls") 16 | .register(meterRegistry); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sample-service/src/main/java/net/trajano/swarm/sampleservice/EmployeeController.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.sampleservice; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RestController; 9 | import reactor.core.publisher.Flux; 10 | import reactor.core.publisher.Mono; 11 | 12 | record Employee(String id, String name, String description) {} 13 | 14 | @Component 15 | @RestController 16 | public class EmployeeController { 17 | 18 | private static final List DB = new ArrayList<>(); 19 | 20 | static { 21 | DB.add(new Employee("1", "Frodo", "ring bearer")); 22 | DB.add(new Employee("2", "Bilbo", "burglar")); 23 | } 24 | 25 | @GetMapping("/api/employees") 26 | Flux all() { 27 | 28 | return Flux.fromIterable(DB); 29 | } 30 | 31 | @GetMapping("/api/employees/{id}") 32 | Mono one(@PathVariable String id) { 33 | 34 | return Flux.fromIterable(DB).filter(employee -> employee.id().equals(id)).last(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sample-service/src/main/java/net/trajano/swarm/sampleservice/GrpcServiceMethod.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.sampleservice; 2 | 3 | public record GrpcServiceMethod(String serviceName, String methodName) {} 4 | -------------------------------------------------------------------------------- /sample-service/src/main/java/net/trajano/swarm/sampleservice/SampleServiceApplication.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.sampleservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SampleServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SampleServiceApplication.class, args); 11 | } 12 | 13 | // @Bean 14 | // GrpcTracing grpcTracing(Tracing tracing) { 15 | // 16 | // return GrpcTracing.create(tracing); 17 | // } 18 | } 19 | -------------------------------------------------------------------------------- /sample-service/src/main/java/net/trajano/swarm/sampleservice/echo/EchoRequest.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.sampleservice.echo; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class EchoRequest { 7 | private String message; 8 | } 9 | -------------------------------------------------------------------------------- /sample-service/src/main/java/net/trajano/swarm/sampleservice/echo/EchoResponse.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.sampleservice.echo; 2 | 3 | import java.time.Instant; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @Builder 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class EchoResponse { 14 | 15 | private String message; 16 | 17 | private Instant timestamp; 18 | } 19 | -------------------------------------------------------------------------------- /sample-service/src/main/proto/echo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package net.trajano.swarm.sampleservice; 3 | 4 | message EchoRequest { 5 | string message = 1; 6 | } 7 | 8 | message EchoResponse { 9 | string message = 1; 10 | } 11 | 12 | service Echo { 13 | rpc echo(EchoRequest) returns (EchoResponse); 14 | rpc echoStream(EchoRequest) returns (stream EchoResponse); 15 | } -------------------------------------------------------------------------------- /sample-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: sample-service 4 | main: 5 | banner-mode: "off" 6 | jmx: 7 | enabled: true 8 | management: 9 | endpoint: 10 | health: 11 | enabled: true 12 | endpoints: 13 | jmx: 14 | exposure: 15 | include: "*" 16 | -------------------------------------------------------------------------------- /sample-service/src/test/java/net/trajano/swarm/sampleservice/SampleServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package net.trajano.swarm.sampleservice; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class SampleServiceApplicationTests { 8 | 9 | @Test 10 | void contextLoads() {} 11 | } 12 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.gradle.enterprise" version "3.9" 3 | } 4 | rootProject.name = 'spring-docker-swarm' 5 | include( 6 | 'gateway', 7 | 'gateway-common', 8 | 'sample-service', 9 | 'grpc-service', 10 | 'logging', 11 | 'dynamic-grpc-client', 12 | 'spring-redis-region', 13 | 'jwks-provider' 14 | ) 15 | 16 | 17 | dependencyResolutionManagement { 18 | versionCatalogs { 19 | libs { 20 | plugin('springBoot', 'org.springframework.boot').version('3.0.3') 21 | plugin('springDependencyManagement', 'io.spring.dependency-management').version('1.1.0') 22 | plugin('graalvm', 'org.graalvm.buildtools.native').version('0.9.18') 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spring-redis-region/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /spring-redis-region/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'io.spring.dependency-management' version '1.1.0' 3 | id 'net.trajano.swarm.conventions' 4 | id 'java-library' 5 | } 6 | 7 | group = 'net.trajano.swarm' 8 | version = '0.0.1-SNAPSHOT' 9 | 10 | configurations { 11 | compileOnly { 12 | extendsFrom annotationProcessor 13 | } 14 | } 15 | 16 | jar { 17 | enabled = true 18 | } 19 | dependencies { 20 | api 'org.springframework.boot:spring-boot' 21 | api 'org.springframework.data:spring-data-redis' 22 | api 'org.springframework:spring-web' 23 | api 'org.hibernate.orm:hibernate-core' 24 | compileOnly 'org.projectlombok:lombok' 25 | annotationProcessor 'org.projectlombok:lombok' 26 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 27 | } 28 | 29 | dependencyManagement { 30 | imports { 31 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 32 | mavenBom "org.springframework.boot:spring-boot-dependencies:${springBootVersion}" 33 | } 34 | } 35 | 36 | tasks.named('test') { 37 | useJUnitPlatform() 38 | } 39 | -------------------------------------------------------------------------------- /spring-redis-region/src/main/java/net/trajano/swarm/spring/redisregion/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This provides the use of Jedis as a Hibernate Cache. The main difference is the configuration is 3 | * done through Spring rather than explicitly set. 4 | * 5 | * 10 | */ 11 | package net.trajano.swarm.spring.redisregion; 12 | -------------------------------------------------------------------------------- /src/openapi/spring-cloud-docker-swarm.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Spring Cloud Docker Swarm 4 | version: 3.0.0 5 | description: | 6 | An implementation of Spring Cloud that utilizes Docker Swarm for service discovery and provides OAuth-like 7 | functionality for controlling access to the system. 8 | 9 | This is using the "SimpleIdentityProvider" which is used for demo and testing purposes. 10 | license: 11 | name: EPL-2.0 12 | url: https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt 13 | servers: 14 | - description: local direct 15 | url: http://localhost:28082 16 | variables: {} 17 | paths: 18 | /auth: 19 | $ref: "./simple-auth.yaml#/paths/~1auth" 20 | /refresh: 21 | $ref: "./auth.yaml#/paths/~1refresh" 22 | /logout: 23 | $ref: "./auth.yaml#/paths/~1logout" 24 | /whoami: 25 | $ref: "./whoami.yaml#/paths/~1whoami" 26 | -------------------------------------------------------------------------------- /src/puml/gateway-redis.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | entity SigningKeyBlock <> { 3 | epochSecondsBlock: long 4 | -- 5 | jwks: JSON Web Key Set 6 | <> expiresOn: Instant 7 | } 8 | note left of SigningKeyBlock::jwks 9 | This contains a public/private 10 | keypair suitable for signature 11 | generation 12 | end note 13 | 14 | entity UserSession <> { 15 | jwtId : UUID 16 | -- 17 | secretClaims : JSON 18 | issuedOn : Instant 19 | verificationJwk : JSON Web Key 20 | <> expiresOn: Instant 21 | } 22 | note right of UserSession::verificationJwk 23 | This contains the public key only 24 | suitable for verification 25 | end note 26 | 27 | 28 | entity Acme <> { 29 | caEndpoint : URL 30 | names: List 31 | -- 32 | keyStore 33 | 34 | challengeToken 35 | challengeAuthorization 36 | 37 | keyStore SHA512 38 | nodeUpdating: boolean 39 | 40 | userKeyPair 41 | domainKeyPair 42 | tosUrl 43 | certificate 44 | } 45 | 46 | @enduml -------------------------------------------------------------------------------- /src/puml/grpc.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | participant "Token\nServer" as auth 4 | participant "Identity\nProvider" as ip 5 | participant "Services" as services 6 | 7 | 8 | @enduml -------------------------------------------------------------------------------- /src/puml/traefik-middlware.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | actor "App/Client" as app 4 | boundary "Traefik" as traefik 5 | participant "Token\nServer" as auth 6 | participant "Identity\nProvider" as ip 7 | participant "Services" as services 8 | 9 | app -> traefik ++: /auth 10 | traefik -> auth ++ : /auth 11 | auth -> ip ++ : credentials 12 | return sessionID\nor other secrets\nfor reauthentication 13 | return OAuthToken 14 | return OAuthToken 15 | 16 | app -> traefik ++: /refresh\n+ refresh_token 17 | traefik -> auth ++ : /refresh\n+ refresh_token 18 | auth -> ip ++ : sessionID\nor other secrets\nfor reauthentication 19 | return updated sessionID\nor other secrets\nfor reauthentication 20 | return OAuthToken 21 | return OAuthToken 22 | 23 | app -> traefik ++: /service/call\n+ access token 24 | traefik -> auth ++ : /service/call\n+ access token 25 | auth -> auth : validate token\n(just JWT signature) 26 | return 200 OK 27 | traefik -> services ++ : /service/call + token 28 | return service response 29 | return service response 30 | 31 | app -> traefik ++: /logout\n+ refresh_token 32 | traefik -> auth ++ : /logout\n+ refresh_token 33 | auth -> ip ++ : sessionID\nor other secrets\nfor logout 34 | return 200 OK 35 | return 200 OK 36 | return 200 OK 37 | 38 | 39 | @enduml --------------------------------------------------------------------------------