├── .compose ├── grafana │ └── provisioning │ │ ├── dashboards │ │ ├── baywatch_dashboard.json │ │ └── spring-dashboard.json │ │ └── datasources │ │ └── datasource.yml ├── loki │ └── local-config.yaml ├── opentelemetry │ ├── exporter_logging.yaml │ ├── exporter_loki.yaml │ ├── exporter_prometheus.yaml │ ├── processor_attributes.yaml │ ├── processor_batch.yaml │ ├── receiver_filelog_container.yaml │ ├── receiver_prometheus.yaml │ └── service.yaml └── prometheus │ └── prometheus.yml ├── .docs ├── baywatch-capture-01.webp └── github-social.png ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ └── gitleaks.yml ├── .gitignore ├── .gitleaks.toml ├── .run ├── All in sandside.run.xml ├── BaywatchApplication.run.xml └── seaside serve.run.xml ├── .tool-versions ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── assembly ├── pom.xml └── src │ └── main │ └── resources │ └── assemblies │ └── baywatch.xml ├── docker-compose.yml ├── http-client.env.json ├── pom.xml ├── sandside ├── lombok.config ├── pom.xml └── src │ ├── main │ ├── java │ │ ├── db │ │ │ └── migration │ │ │ │ └── V2_0_1__create_default_users.java │ │ └── fr │ │ │ └── ght1pc9kc │ │ │ └── baywatch │ │ │ ├── BaywatchApplication.java │ │ │ ├── admin │ │ │ ├── api │ │ │ │ ├── FeedManagementService.java │ │ │ │ ├── StatisticsService.java │ │ │ │ └── model │ │ │ │ │ ├── Counter.java │ │ │ │ │ ├── CounterGroup.java │ │ │ │ │ ├── CounterProvider.java │ │ │ │ │ └── RawFeed.java │ │ │ ├── domain │ │ │ │ ├── NoCounterProvider.java │ │ │ │ ├── UptimeCounterProvider.java │ │ │ │ ├── ports │ │ │ │ │ ├── AdministrationProxifierPort.java │ │ │ │ │ ├── RawFeedPersistencePort.java │ │ │ │ │ └── RawNewsPersistencePort.java │ │ │ │ └── services │ │ │ │ │ ├── FeedManagementServiceImpl.java │ │ │ │ │ └── StatisticsServiceImpl.java │ │ │ └── infra │ │ │ │ ├── adapters │ │ │ │ ├── AdministrationProxifierAdapter.java │ │ │ │ ├── FeedManagementAdapter.java │ │ │ │ ├── RawFeedPersistenceAdapter.java │ │ │ │ ├── RawNewsPersistenceAdapter.java │ │ │ │ ├── StatisticsServiceAdapter.java │ │ │ │ └── UptimeCounterAdapter.java │ │ │ │ ├── config │ │ │ │ └── GraphqlAdminConfig.java │ │ │ │ ├── controllers │ │ │ │ ├── FeedManagementController.java │ │ │ │ └── StatisticsController.java │ │ │ │ ├── mappers │ │ │ │ └── RawFeedMapper.java │ │ │ │ └── model │ │ │ │ ├── AdminRawFeedForm.java │ │ │ │ ├── AdminRawFeedRequest.java │ │ │ │ └── Statistics.java │ │ │ ├── common │ │ │ ├── api │ │ │ │ ├── DefaultMeta.java │ │ │ │ ├── HttpHeaders.java │ │ │ │ ├── HttpStatusCodes.java │ │ │ │ ├── LocaleFacade.java │ │ │ │ ├── exceptions │ │ │ │ │ └── UnauthorizedException.java │ │ │ │ └── model │ │ │ │ │ ├── BaywatchLogsMarkers.java │ │ │ │ │ ├── ClientInfoContext.java │ │ │ │ │ ├── EntitiesProperties.java │ │ │ │ │ ├── FeedMeta.java │ │ │ │ │ ├── HeroIcons.java │ │ │ │ │ └── UserMeta.java │ │ │ ├── domain │ │ │ │ ├── DateUtils.java │ │ │ │ ├── EntityAssert.java │ │ │ │ ├── Failure.java │ │ │ │ ├── Hasher.java │ │ │ │ ├── MailAddress.java │ │ │ │ ├── QueryContext.java │ │ │ │ ├── Success.java │ │ │ │ ├── Try.java │ │ │ │ └── exceptions │ │ │ │ │ ├── BadRequestCriteria.java │ │ │ │ │ └── HashingException.java │ │ │ └── infra │ │ │ │ ├── DatabaseQualifier.java │ │ │ │ ├── adapters │ │ │ │ ├── GraphqlExceptionAdapter.java │ │ │ │ ├── LocaleFacadeAdapter.java │ │ │ │ └── PerformanceJooqListener.java │ │ │ │ ├── config │ │ │ │ ├── GraphqlConfiguration.java │ │ │ │ ├── SpringConfiguration.java │ │ │ │ ├── jackson │ │ │ │ │ ├── JacksonMappingConfiguration.java │ │ │ │ │ └── LocaleToLanguageTagSerializer.java │ │ │ │ └── scalars │ │ │ │ │ └── URIScalar.java │ │ │ │ ├── filters │ │ │ │ ├── ClientInfoContextWebFilter.java │ │ │ │ ├── HomePageWebFilter.java │ │ │ │ ├── LocaleContextWebFilter.java │ │ │ │ ├── ReactiveClientInfoContextHolder.java │ │ │ │ └── ReactiveLocaleContextHolder.java │ │ │ │ ├── mappers │ │ │ │ ├── BaywatchMapper.java │ │ │ │ └── PropertiesMappers.java │ │ │ │ └── model │ │ │ │ ├── CreateValidation.java │ │ │ │ ├── Page.java │ │ │ │ ├── PatchOperation.java │ │ │ │ ├── PatchPayload.java │ │ │ │ ├── ResourcePatch.java │ │ │ │ └── UpdateValidation.java │ │ │ ├── indexer │ │ │ ├── api │ │ │ │ ├── FeedIndexerService.java │ │ │ │ └── FeedSearchService.java │ │ │ ├── domain │ │ │ │ ├── FeedIndexerServiceImpl.java │ │ │ │ ├── FeedSearchServiceImpl.java │ │ │ │ ├── model │ │ │ │ │ ├── EntryDocument.java │ │ │ │ │ ├── FeedDocument.java │ │ │ │ │ ├── Indexable.java │ │ │ │ │ ├── IndexableDocument.java │ │ │ │ │ ├── IndexableFeed.java │ │ │ │ │ ├── IndexableFeedEntry.java │ │ │ │ │ └── IndexableVisitor.java │ │ │ │ └── ports │ │ │ │ │ ├── IndexBuilderPort.java │ │ │ │ │ ├── IndexSearcherPort.java │ │ │ │ │ └── IndexableDataPort.java │ │ │ └── infra │ │ │ │ ├── adapters │ │ │ │ ├── FeedIndexerServiceAdapter.java │ │ │ │ ├── FeedSearchServiceAdapter.java │ │ │ │ ├── IndexableDataAdapter.java │ │ │ │ ├── IndexerMapper.java │ │ │ │ └── LuceneDataAdapter.java │ │ │ │ ├── config │ │ │ │ └── IndexerProperties.java │ │ │ │ ├── controllers │ │ │ │ ├── IndexerController.java │ │ │ │ └── SearchIndexController.java │ │ │ │ ├── handlers │ │ │ │ └── IndexerEventHandler.java │ │ │ │ └── model │ │ │ │ └── IndexEntry.java │ │ │ ├── notify │ │ │ ├── api │ │ │ │ ├── NotifyManager.java │ │ │ │ ├── NotifyService.java │ │ │ │ └── model │ │ │ │ │ ├── BasicEvent.java │ │ │ │ │ ├── EventType.java │ │ │ │ │ ├── ReactiveEvent.java │ │ │ │ │ ├── ServerEvent.java │ │ │ │ │ ├── ServerEventVisitor.java │ │ │ │ │ ├── Severity.java │ │ │ │ │ └── UserNotification.java │ │ │ ├── domain │ │ │ │ ├── NotifyServiceImpl.java │ │ │ │ └── ports │ │ │ │ │ └── NotificationPersistencePort.java │ │ │ └── infra │ │ │ │ ├── NotificationController.java │ │ │ │ ├── SseController.java │ │ │ │ └── adapters │ │ │ │ ├── NotificationPersistenceAdapter.java │ │ │ │ └── NotifyServiceAdapter.java │ │ │ ├── opml │ │ │ ├── api │ │ │ │ └── OpmlService.java │ │ │ ├── domain │ │ │ │ ├── OPMLTags.java │ │ │ │ ├── OpmlExecption.java │ │ │ │ ├── OpmlReader.java │ │ │ │ ├── OpmlReaderFactory.java │ │ │ │ ├── OpmlServiceImpl.java │ │ │ │ ├── OpmlWriter.java │ │ │ │ └── OpmlWriterFactory.java │ │ │ └── infra │ │ │ │ ├── OpmlController.java │ │ │ │ └── OpmlServiceAdapter.java │ │ │ ├── scraper │ │ │ ├── api │ │ │ │ ├── FeedScraperPlugin.java │ │ │ │ ├── FeedScraperService.java │ │ │ │ ├── NewsFilter.java │ │ │ │ ├── RssAtomParser.java │ │ │ │ ├── ScrapEnrichmentService.java │ │ │ │ ├── ScrapingErrorsService.java │ │ │ │ ├── ScrapingEventHandler.java │ │ │ │ └── model │ │ │ │ │ ├── AtomEntry.java │ │ │ │ │ ├── AtomFeed.java │ │ │ │ │ ├── ScrapResult.java │ │ │ │ │ ├── ScrapingError.java │ │ │ │ │ └── ScrapingEventType.java │ │ │ ├── domain │ │ │ │ ├── AtomFeedReducer.java │ │ │ │ ├── FeedScraperServiceImpl.java │ │ │ │ ├── RssAtomParserImpl.java │ │ │ │ ├── ScrapEnrichmentServiceImpl.java │ │ │ │ ├── ScrapingErrorsServiceImpl.java │ │ │ │ ├── actions │ │ │ │ │ ├── DeleteOrphanFeedHandler.java │ │ │ │ │ ├── NewsUpdateNotificationHandler.java │ │ │ │ │ ├── PersistErrorsHandler.java │ │ │ │ │ ├── PurgeNewsHandler.java │ │ │ │ │ ├── ScrapingDurationCounter.java │ │ │ │ │ └── ScrapingLoggerHandler.java │ │ │ │ ├── filters │ │ │ │ │ ├── FaviconFeedFilter.java │ │ │ │ │ ├── ImageLinkValidationFilter.java │ │ │ │ │ ├── OpenGraphFilter.java │ │ │ │ │ ├── RedditNewsFilter.java │ │ │ │ │ └── SanitizerFilter.java │ │ │ │ ├── model │ │ │ │ │ ├── FeedsFilter.java │ │ │ │ │ ├── Publishable.java │ │ │ │ │ ├── RssNamespaces.java │ │ │ │ │ ├── ScrapedFeed.java │ │ │ │ │ ├── ScraperProperties.java │ │ │ │ │ └── ex │ │ │ │ │ │ ├── FeedScrapingException.java │ │ │ │ │ │ ├── NewsScrapingException.java │ │ │ │ │ │ ├── ScrapingException.java │ │ │ │ │ │ └── ScrapingExceptionCode.java │ │ │ │ ├── plugins │ │ │ │ │ └── RedditParserPlugin.java │ │ │ │ └── ports │ │ │ │ │ ├── LinkCheckPort.java │ │ │ │ │ ├── ScraperMaintenancePort.java │ │ │ │ │ ├── ScrapingAuthentFacade.java │ │ │ │ │ └── ScrapingErrorPersistencePort.java │ │ │ └── infra │ │ │ │ ├── adapters │ │ │ │ ├── LinkCheckAdapter.java │ │ │ │ ├── ScrapingAuthentFacadeAdapter.java │ │ │ │ ├── handlers │ │ │ │ │ ├── DeleteOrphanFeedHandlerAdapter.java │ │ │ │ │ ├── NewsUpdateNotificationHandlerAdapter.java │ │ │ │ │ ├── PersistErrorsHandlerAdapter.java │ │ │ │ │ ├── PurgeNewsHandlerAdapter.java │ │ │ │ │ ├── ScraperExceptionResolverAdapter.java │ │ │ │ │ ├── ScrapingDurationAdapter.java │ │ │ │ │ └── ScrapingLoggerHandlerAdapter.java │ │ │ │ ├── persistence │ │ │ │ │ └── ScrapingErrorPersistenceAdapter.java │ │ │ │ └── services │ │ │ │ │ ├── FeedScraperServiceAdapter.java │ │ │ │ │ ├── RssAtomParserAdapter.java │ │ │ │ │ ├── ScrapEnrichmentAdapter.java │ │ │ │ │ ├── ScraperMaintenanceAdapter.java │ │ │ │ │ └── ScrapingErrorsServiceAdapter.java │ │ │ │ ├── config │ │ │ │ ├── OpenGraphConfig.java │ │ │ │ ├── ScraperApplicationProperties.java │ │ │ │ ├── ScraperConfiguration.java │ │ │ │ ├── ScraperQualifier.java │ │ │ │ └── WebClientConfiguration.java │ │ │ │ ├── controllers │ │ │ │ ├── ScrapController.java │ │ │ │ ├── ScraperGqlController.java │ │ │ │ ├── ScraperTaskScheduler.java │ │ │ │ └── ScrapingErrorsController.java │ │ │ │ └── mappers │ │ │ │ └── ScraperMapper.java │ │ │ ├── security │ │ │ ├── PasswordChecker.java │ │ │ ├── api │ │ │ │ ├── AuthenticationFacade.java │ │ │ │ ├── AuthenticationService.java │ │ │ │ ├── AuthorizationService.java │ │ │ │ ├── PasswordService.java │ │ │ │ ├── UserService.java │ │ │ │ ├── UserSettingsService.java │ │ │ │ └── model │ │ │ │ │ ├── AuthenticationRequest.java │ │ │ │ │ ├── Authorization.java │ │ │ │ │ ├── BaywatchAuthentication.java │ │ │ │ │ ├── NewsViewType.java │ │ │ │ │ ├── PasswordEvaluation.java │ │ │ │ │ ├── Permission.java │ │ │ │ │ ├── Role.java │ │ │ │ │ ├── RoleUtils.java │ │ │ │ │ ├── User.java │ │ │ │ │ └── UserSettings.java │ │ │ ├── domain │ │ │ │ ├── AuthenticationServiceImpl.java │ │ │ │ ├── JwtBaywatchAuthenticationProviderImpl.java │ │ │ │ ├── PasswordServiceImpl.java │ │ │ │ ├── UserServiceImpl.java │ │ │ │ ├── UserSettingsServiceImpl.java │ │ │ │ ├── exceptions │ │ │ │ │ ├── ConstraintViolationPersistenceException.java │ │ │ │ │ ├── SecurityException.java │ │ │ │ │ ├── UnauthenticatedUser.java │ │ │ │ │ ├── UnauthorizedOperation.java │ │ │ │ │ └── UserCreateException.java │ │ │ │ ├── model │ │ │ │ │ └── JwtProperties.java │ │ │ │ └── ports │ │ │ │ │ ├── AuthenticationManagerPort.java │ │ │ │ │ ├── AuthorizationPersistencePort.java │ │ │ │ │ ├── JwtTokenProvider.java │ │ │ │ │ ├── NotificationPort.java │ │ │ │ │ ├── PasswordStrengthChecker.java │ │ │ │ │ ├── UserPersistencePort.java │ │ │ │ │ └── UserSettingsPersistencePort.java │ │ │ └── infra │ │ │ │ ├── JwtTokenAuthenticationFilter.java │ │ │ │ ├── TokenCookieManager.java │ │ │ │ ├── adapters │ │ │ │ ├── AuthenticationManagerAdapter.java │ │ │ │ ├── AuthenticationServiceAdapter.java │ │ │ │ ├── JwtTokenProviderAdapter.java │ │ │ │ ├── NotificationServiceAdapter.java │ │ │ │ ├── PasswordCheckerNbvcxz.java │ │ │ │ ├── PasswordServiceAdapter.java │ │ │ │ ├── SecurityExceptionResolver.java │ │ │ │ ├── SecurityMetricsAdapter.java │ │ │ │ ├── SpringAuthenticationContext.java │ │ │ │ ├── UserServiceAdapter.java │ │ │ │ ├── UserSettingsServiceAdapter.java │ │ │ │ └── UsersCounterProvider.java │ │ │ │ ├── config │ │ │ │ ├── GraphqlSecurityConfig.java │ │ │ │ ├── JacksonSecurityConfig.java │ │ │ │ ├── PermissionMixin.java │ │ │ │ ├── SecurityConfiguration.java │ │ │ │ ├── SecurityMapper.java │ │ │ │ └── UserMixin.java │ │ │ │ ├── controllers │ │ │ │ ├── AuthenticationGqlController.java │ │ │ │ ├── PasswordController.java │ │ │ │ ├── UserController.java │ │ │ │ ├── UserGqlController.java │ │ │ │ └── UserSettingsController.java │ │ │ │ ├── exceptions │ │ │ │ ├── AlreadyExistsException.java │ │ │ │ ├── BaywatchCredentialsException.java │ │ │ │ └── NoSessionException.java │ │ │ │ ├── mappers │ │ │ │ ├── UserMapper.java │ │ │ │ └── UserSettingsMapper.java │ │ │ │ ├── model │ │ │ │ ├── BaywatchUserDetails.java │ │ │ │ ├── ChangePasswordForm.java │ │ │ │ ├── SecurityParams.java │ │ │ │ ├── Session.java │ │ │ │ ├── UserForm.java │ │ │ │ ├── UserSearchRequest.java │ │ │ │ └── UserSettingsForm.java │ │ │ │ └── persistence │ │ │ │ ├── AuthorizationRepository.java │ │ │ │ ├── UserRepository.java │ │ │ │ └── UserSettingsRepository.java │ │ │ ├── teams │ │ │ ├── api │ │ │ │ ├── TeamsAdminService.java │ │ │ │ ├── TeamsService.java │ │ │ │ ├── exceptions │ │ │ │ │ └── TeamPermissionDenied.java │ │ │ │ └── model │ │ │ │ │ ├── Team.java │ │ │ │ │ └── TeamMember.java │ │ │ ├── domain │ │ │ │ ├── TeamServiceImpl.java │ │ │ │ ├── model │ │ │ │ │ └── PendingFor.java │ │ │ │ └── ports │ │ │ │ │ ├── TeamAuthFacade.java │ │ │ │ │ ├── TeamMemberPersistencePort.java │ │ │ │ │ └── TeamPersistencePort.java │ │ │ └── infra │ │ │ │ ├── adapters │ │ │ │ ├── MembersPersistenceAdapter.java │ │ │ │ ├── TeamAuthFacadeAdapter.java │ │ │ │ ├── TeamPersistenceAdapter.java │ │ │ │ └── TeamsServiceAdapter.java │ │ │ │ ├── controllers │ │ │ │ ├── TeamMembersController.java │ │ │ │ ├── TeamsController.java │ │ │ │ └── UserMappingController.java │ │ │ │ ├── mappers │ │ │ │ ├── PropertiesMapper.java │ │ │ │ └── TeamsMapper.java │ │ │ │ └── model │ │ │ │ ├── SearchTeamsRequest.java │ │ │ │ └── TeamForm.java │ │ │ └── techwatch │ │ │ ├── api │ │ │ ├── FeedService.java │ │ │ ├── ImageProxyService.java │ │ │ ├── NewsService.java │ │ │ ├── PopularNewsService.java │ │ │ ├── SystemMaintenanceService.java │ │ │ └── model │ │ │ │ ├── Flags.java │ │ │ │ ├── ImagePresets.java │ │ │ │ ├── ImageProxyProperties.java │ │ │ │ ├── News.java │ │ │ │ ├── Popularity.java │ │ │ │ ├── RawNews.java │ │ │ │ ├── State.java │ │ │ │ └── WebFeed.java │ │ │ ├── domain │ │ │ ├── FeedServiceImpl.java │ │ │ ├── ImageProxyServiceImpl.java │ │ │ ├── NewsServiceImpl.java │ │ │ ├── PopularNewsServiceImpl.java │ │ │ ├── SystemMaintenanceServiceImpl.java │ │ │ └── ports │ │ │ │ ├── FeedPersistencePort.java │ │ │ │ ├── NewsPersistencePort.java │ │ │ │ ├── ScraperServicePort.java │ │ │ │ ├── StatePersistencePort.java │ │ │ │ └── TeamServicePort.java │ │ │ └── infra │ │ │ ├── adapters │ │ │ ├── FeedsCounterProvider.java │ │ │ ├── ImageProxyAdapter.java │ │ │ ├── NewsCounterProvider.java │ │ │ ├── PopularNewsServiceAdapter.java │ │ │ ├── SystemMaintenanceAdapter.java │ │ │ ├── TechwatchExceptionResolverAdapter.java │ │ │ ├── TechwatchMetricsAdapter.java │ │ │ ├── persistence │ │ │ │ ├── FeedConditionsVisitors.java │ │ │ │ ├── FeedRepository.java │ │ │ │ ├── NewsRepository.java │ │ │ │ └── StateRepository.java │ │ │ └── services │ │ │ │ ├── FeedServiceAdapter.java │ │ │ │ ├── NewsServiceAdapter.java │ │ │ │ ├── ScraperServiceAdapter.java │ │ │ │ └── TeamServiceAdapter.java │ │ │ ├── config │ │ │ ├── GraphqlTechwatchConfig.java │ │ │ ├── ImageProxyConfig.java │ │ │ ├── TechwatchJacksonMappingConfiguration.java │ │ │ └── TechwatchMapper.java │ │ │ ├── controllers │ │ │ ├── FeedController.java │ │ │ ├── GraphQLFeedsController.java │ │ │ ├── GraphQLNewsController.java │ │ │ ├── ImageProxyGqlMapper.java │ │ │ ├── NewsController.java │ │ │ └── TagsController.java │ │ │ └── model │ │ │ ├── FeedDeletedResult.java │ │ │ ├── FeedForm.java │ │ │ ├── FeedMixin.java │ │ │ ├── FeedProperties.java │ │ │ ├── NewsMixin.java │ │ │ ├── StateMixin.java │ │ │ └── graphql │ │ │ ├── SearchFeedsRequest.java │ │ │ └── SearchNewsRequest.java │ └── resources │ │ ├── application.yaml │ │ ├── db │ │ └── migration │ │ │ ├── V2_0_0__baseline.sql │ │ │ ├── V2_1_202312091459__add_notifications.sql │ │ │ ├── V2_1_202402062210__upgrade_users.sql │ │ │ ├── V2_1_202402171635__nefe_feed_idx.sql │ │ │ ├── V2_1_202403252236__feed_arrors.sql │ │ │ ├── V2_1_202408181554__user_settings.sql │ │ │ ├── V2_1_202409011551__feeds_users_properties.sql │ │ │ ├── V2_1_202410201508__users_settings_autoread.sql │ │ │ ├── V2_1_202410262040__users_settings_news_view.sql │ │ │ ├── V2_2_202502231146__add_icon_to_feeds.sql │ │ │ └── V2_2_202505132142__update_feed_arrors.sql │ │ ├── graphql │ │ ├── admin │ │ │ ├── feedManagement.graphqls │ │ │ └── statistics.graphqls │ │ ├── common │ │ │ ├── level.graphqls │ │ │ └── scalars.graphqls │ │ ├── indexer │ │ │ └── indexer.graphqls │ │ ├── main.graphqls │ │ ├── scraper │ │ │ └── scraper.graphqls │ │ ├── security │ │ │ ├── _model.graphqls │ │ │ ├── authentication.graphqls │ │ │ ├── password.graphqls │ │ │ ├── settings.graphqls │ │ │ └── user.graphqls │ │ ├── teams │ │ │ ├── _model.graphqls │ │ │ ├── members.graphqls │ │ │ └── teams.graphqls │ │ └── techwatch │ │ │ ├── feeds.graphqls │ │ │ └── news.graphqls │ │ ├── logback-spring.xml │ │ └── templates │ │ └── index.html │ └── test │ ├── java │ └── fr │ │ └── ght1pc9kc │ │ └── baywatch │ │ ├── BaywatchApplicationTests.java │ │ ├── admin │ │ ├── domain │ │ │ └── services │ │ │ │ ├── FeedManagementServiceImplTest.java │ │ │ │ └── StatisticsServiceImplTest.java │ │ └── infra │ │ │ ├── adapters │ │ │ └── AdministrationProxifierAdapterTest.java │ │ │ ├── controllers │ │ │ └── FeedManagementControllerTest.java │ │ │ └── mappers │ │ │ └── RawFeedMapperTest.java │ │ ├── common │ │ ├── domain │ │ │ ├── HasherTest.java │ │ │ ├── MailAddressTest.java │ │ │ └── QueryContextTest.java │ │ └── infra │ │ │ ├── config │ │ │ └── scalars │ │ │ │ └── URIScalarTest.java │ │ │ ├── filters │ │ │ └── ReactiveClientInfoContextHolderTest.java │ │ │ ├── mappers │ │ │ ├── DateUtilsTest.java │ │ │ └── NewsToRecordConverterTest.java │ │ │ └── model │ │ │ └── PatchPayloadTest.java │ │ ├── indexer │ │ ├── domain │ │ │ └── FeedIndexerServiceImplTest.java │ │ └── infra │ │ │ ├── adapters │ │ │ ├── IndexableDataAdapterTest.java │ │ │ └── IndexerMapperTest.java │ │ │ ├── controllers │ │ │ └── IndexerControllerTest.java │ │ │ └── handlers │ │ │ └── IndexerEventHandlerTest.java │ │ ├── notify │ │ ├── domain │ │ │ └── NotifyServiceImplTest.java │ │ └── infra │ │ │ ├── NotificationControllerTest.java │ │ │ ├── adapters │ │ │ └── NotificationPersistenceAdapterTest.java │ │ │ └── samples │ │ │ └── NotificationsRecordSamples.java │ │ ├── opml │ │ └── domain │ │ │ ├── OpmlServiceImplTest.java │ │ │ └── OpmlWriterTest.java │ │ ├── scraper │ │ ├── domain │ │ │ ├── FeedScraperServiceTest.java │ │ │ ├── RssAtomParserImplTest.java │ │ │ ├── ScrapEnrichmentServiceImplTest.java │ │ │ ├── ScrapingErrorsServiceImplTest.java │ │ │ ├── actions │ │ │ │ ├── DeleteOrphanFeedHandlerTest.java │ │ │ │ ├── NewsUpdateNotificationHandlerTest.java │ │ │ │ ├── PersistErrorsHandlerTest.java │ │ │ │ ├── PurgeNewsActionTest.java │ │ │ │ ├── ScrapingDurationCounterTest.java │ │ │ │ └── ScrapingLoggerHandlerTest.java │ │ │ ├── filters │ │ │ │ ├── ImageLinkValidationFilterTest.java │ │ │ │ ├── OpenGraphFilterTest.java │ │ │ │ ├── RedditNewsFilterTest.java │ │ │ │ └── SanitizerFilterTest.java │ │ │ └── plugins │ │ │ │ └── RedditParserPluginTest.java │ │ └── infra │ │ │ ├── FeedScraperIntegrationTest.java │ │ │ ├── adapters │ │ │ ├── ScrapingErrorPersistenceAdapterTest.java │ │ │ ├── handlers │ │ │ │ └── ScraperExceptionResolverAdapterTest.java │ │ │ └── services │ │ │ │ └── ScraperMaintenanceAdapterTest.java │ │ │ ├── config │ │ │ └── ScraperMapperTest.java │ │ │ └── controllers │ │ │ └── ScrapingErrorsControllerTest.java │ │ ├── security │ │ ├── api │ │ │ └── model │ │ │ │ ├── PermissionTest.java │ │ │ │ └── RoleUtilsTest.java │ │ ├── domain │ │ │ ├── AuthenticationServiceImplTest.java │ │ │ ├── JwtBaywatchAuthenticationProviderImplTest.java │ │ │ ├── PasswordServiceImplTest.java │ │ │ ├── UserServiceImplTest.java │ │ │ └── UserSettingsServiceImplTest.java │ │ └── infra │ │ │ ├── JwtTokenAuthenticationFilterTest.java │ │ │ ├── TokenCookieManagerTest.java │ │ │ ├── adapters │ │ │ ├── AuthenticationManagerAdapterTest.java │ │ │ └── SpringAuthenticationContextTest.java │ │ │ ├── config │ │ │ └── PermissionMixinTest.java │ │ │ ├── controllers │ │ │ ├── AuthenticationGqlControllerTest.java │ │ │ └── UserGqlControllerTest.java │ │ │ └── persistence │ │ │ ├── UserRepositoryTest.java │ │ │ └── UserSettingsRepositoryTest.java │ │ ├── teams │ │ ├── domain │ │ │ └── TeamServiceImplTest.java │ │ └── infra │ │ │ ├── controllers │ │ │ └── UserMappingControllerTest.java │ │ │ └── samples │ │ │ ├── TeamsMembersRecordSamples.java │ │ │ └── TeamsRecordSamples.java │ │ ├── techwatch │ │ ├── domain │ │ │ ├── FeedServiceImplTest.java │ │ │ ├── NewsServiceImplTest.java │ │ │ └── SystemMaintenanceServiceImplTest.java │ │ └── infra │ │ │ ├── MockSecurityConfiguration.java │ │ │ ├── config │ │ │ └── TechwatchMapperTest.java │ │ │ ├── controllers │ │ │ └── GraphQLFeedsControllerTest.java │ │ │ └── persistence │ │ │ ├── FeedRepositoryTest.java │ │ │ ├── NewsRepositoryTest.java │ │ │ └── StateRepositoryTest.java │ │ └── tests │ │ ├── extensions │ │ └── URIComparator.java │ │ ├── metrics │ │ ├── MockMeterRegistry.java │ │ └── MockObservationRegistry.java │ │ └── samples │ │ ├── FeedSamples.java │ │ ├── NewsSamples.java │ │ ├── UserSamples.java │ │ └── infra │ │ ├── FeedRecordSamples.java │ │ ├── FeedsErrorsRecordSamples.java │ │ ├── FeedsUsersPropertiesRecordSample.java │ │ ├── FeedsUsersRecordSample.java │ │ ├── NewsRecordSamples.java │ │ ├── UsersRecordSamples.java │ │ ├── UsersRolesSamples.java │ │ └── UsersSettingsRecordSamples.java │ └── resources │ ├── application-test.yaml │ ├── fr │ └── ght1pc9kc │ │ └── baywatch │ │ ├── opml │ │ └── domain │ │ │ └── okenobi.xml │ │ └── scraper │ │ ├── domain │ │ ├── feeds │ │ │ ├── atom_entry.xml │ │ │ ├── atom_entry_with_html_content.xml │ │ │ ├── debian_io.xml │ │ │ ├── entry_with_encoded_char.xml │ │ │ ├── feed_two_digits_year.xml │ │ │ ├── feed_uber.xml │ │ │ ├── journal_du_hacker.xml │ │ │ ├── lemonde.xml │ │ │ ├── librealire-dc.xml │ │ │ ├── malformed_rss_feed.xml │ │ │ ├── reddit-java.xml │ │ │ ├── reddit-prog.xml │ │ │ ├── rss_item.xml │ │ │ ├── rss_item_pubdate_format.xml │ │ │ ├── rss_item_with_cdata.xml │ │ │ ├── rss_item_with_encoded_content.xml │ │ │ ├── rss_item_with_illegal_protocol.xml │ │ │ ├── rss_item_with_relative_link.xml │ │ │ ├── sebosss.xml │ │ │ ├── should_read_rss_item_without_publication_date.xml │ │ │ └── spring-blog.xml │ │ ├── imports │ │ │ └── opml-import-test.xml │ │ └── opengraph │ │ │ └── plugins │ │ │ └── youtube.html │ │ └── infra │ │ ├── entries │ │ ├── 132-blog_devgenius_io │ │ ├── 132-dmitryelj_medium_com │ │ └── 132-http-server-teapot │ │ └── feeds │ │ ├── 132-feed-canonical.xml │ │ ├── 132-http-server-incomplete-teapot.xml │ │ ├── 132-http-server-incomplete.xml │ │ └── 135-invalid-feed-flux.xml │ ├── graphql-test │ ├── feedManagementTest.graphql │ └── feedsServiceTest.graphql │ ├── logback-test.xml │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── seaside ├── .browserslistrc ├── .env ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── README.md ├── index.html ├── jest.config.js ├── package.json ├── pom.xml ├── public │ ├── favicon.ico │ ├── login.webp │ ├── placeholder.svg │ └── robots.txt ├── src │ ├── .env.production.local │ ├── App.vue │ ├── administration │ │ ├── component │ │ │ ├── ConfigAdminTab.vue │ │ │ ├── FeedsAdminTab.vue │ │ │ ├── StatisticsAdminTab.vue │ │ │ ├── UserAdminTab.vue │ │ │ └── usereditor │ │ │ │ ├── AdminFeedEditor.vue │ │ │ │ ├── UserEditor.vue │ │ │ │ └── UserRoleInput.vue │ │ ├── model │ │ │ ├── Counter.type.ts │ │ │ ├── RawFeed.type.ts │ │ │ ├── ScraperError.ts │ │ │ ├── Statistics.type.ts │ │ │ └── UserView.ts │ │ ├── page │ │ │ └── AdministrationPage.vue │ │ ├── router │ │ │ └── index.ts │ │ └── services │ │ │ ├── FeedAdministrationService.ts │ │ │ └── StatisticsService.ts │ ├── assets │ │ ├── logo.png │ │ ├── rescue-buoy.svg │ │ └── styles │ │ │ └── index.css │ ├── common │ │ ├── components │ │ │ ├── BaywatchIcon.vue │ │ │ ├── CurtainModal.vue │ │ │ ├── FeedCard.vue │ │ │ ├── FileUploadWindow.vue │ │ │ ├── ModalWindow.vue │ │ │ ├── TagInput.vue │ │ │ ├── alertdialog │ │ │ │ ├── AlertDialog.types.ts │ │ │ │ ├── AlertDialog.vue │ │ │ │ └── plugin.ts │ │ │ ├── notificationArea │ │ │ │ ├── NotificationArea.vue │ │ │ │ └── NotificationView.ts │ │ │ └── smartTable │ │ │ │ ├── SmartTable.vue │ │ │ │ ├── SmartTableData.vue │ │ │ │ ├── SmartTableLineAction.vue │ │ │ │ └── SmartTableView.interface.ts │ │ ├── errors │ │ │ ├── BadRequestError.ts │ │ │ ├── ForbiddenError.ts │ │ │ ├── HttpStatusError.ts │ │ │ ├── SandSideError.ts │ │ │ ├── UnauthorizedError.ts │ │ │ ├── UnknownFetchError.ts │ │ │ └── ValidationError.ts │ │ ├── model │ │ │ ├── FeedCardView.type.ts │ │ │ ├── GraphqlResponse.type.ts │ │ │ ├── Locale.type.ts │ │ │ ├── NewsViewMode.ts │ │ │ ├── Page.ts │ │ │ ├── SearchRequest.type.ts │ │ │ └── store │ │ │ │ └── NewsStore.type.ts │ │ └── services │ │ │ ├── GraphQLClient.ts │ │ │ ├── KeyboardController.ts │ │ │ ├── RegexPattern.ts │ │ │ ├── ReloadActionService.ts │ │ │ ├── RestWrapper.ts │ │ │ └── common.ts │ ├── configuration │ │ ├── components │ │ │ ├── SettingsTab.vue │ │ │ ├── feedslist │ │ │ │ ├── FeedEditor.vue │ │ │ │ └── FeedsList.vue │ │ │ └── profile │ │ │ │ ├── ChangePasswordModal.vue │ │ │ │ └── ProfileTab.vue │ │ ├── model │ │ │ ├── Feed.type.ts │ │ │ ├── GraphQLScraper.type.ts │ │ │ └── SearchFeedsResponse.type.ts │ │ ├── pages │ │ │ └── ConfigurationPage.vue │ │ ├── router │ │ │ └── index.ts │ │ └── services │ │ │ └── FeedService.ts │ ├── constants.ts │ ├── env.d.ts │ ├── i18n.ts │ ├── layout │ │ ├── components │ │ │ ├── AddSingleNewsAction.vue │ │ │ ├── SearchResultAction.vue │ │ │ ├── TopNavActionOverlay.vue │ │ │ ├── TopNavigationBar.vue │ │ │ └── sidenav │ │ │ │ ├── SideNav.vue │ │ │ │ ├── SideNavFilters.vue │ │ │ │ ├── SideNavHeader.vue │ │ │ │ ├── SideNavManagement.vue │ │ │ │ ├── SideNavOverlay.vue │ │ │ │ └── SideNavUserInfo.vue │ │ ├── model │ │ │ ├── AtomFeed.type.ts │ │ │ ├── Feed.type.ts │ │ │ └── SearchResult.type.ts │ │ └── services │ │ │ ├── ScraperService.ts │ │ │ ├── SearchService.ts │ │ │ └── ServerEventService.ts │ ├── locales │ │ ├── ClippedPage_en-US.ts │ │ ├── ClippedPage_fr-FR.ts │ │ ├── HomePage_en-US.ts │ │ ├── HomePage_fr-FR.ts │ │ ├── LoginPage_en-US.ts │ │ ├── LoginPage_fr-FR.ts │ │ ├── TeamsPage_en-US.ts │ │ ├── TeamsPage_fr-FR.ts │ │ ├── admin-feed-editor_en-US.ts │ │ ├── admin-feed-editor_fr-FR.ts │ │ ├── admin-feeds_en-US.ts │ │ ├── admin-feeds_fr-FR.ts │ │ ├── admin-stats_en-US.ts │ │ ├── admin-stats_fr-FR.ts │ │ ├── admin-users_en-US.ts │ │ ├── admin-users_fr-FR.ts │ │ ├── admin_en-US.ts │ │ ├── admin_fr-FR.ts │ │ ├── backend │ │ │ ├── scraping_en-US.ts │ │ │ └── scraping_fr-FR.ts │ │ ├── components │ │ │ ├── fileuploadwindow_en-US.ts │ │ │ ├── fileuploadwindow_fr-FR.ts │ │ │ ├── smarttable_en-US.ts │ │ │ ├── smarttable_fr-FR.ts │ │ │ ├── taginput_en-US.ts │ │ │ └── taginput_fr-FR.ts │ │ ├── config-feeds_en-US.ts │ │ ├── config-feeds_fr-FR.ts │ │ ├── config-profile_en-US.ts │ │ ├── config-profile_fr-FR.ts │ │ ├── config-settings_en-US.ts │ │ ├── config-settings_fr-FR.ts │ │ ├── config_en-US.ts │ │ ├── config_fr-FR.ts │ │ ├── main_en-US.ts │ │ └── main_fr-FR.ts │ ├── main.ts │ ├── router.ts │ ├── security │ │ ├── components │ │ │ └── CreateAccountComponent.vue │ │ ├── model │ │ │ ├── PasswordEvaluation.type.ts │ │ │ ├── Session.ts │ │ │ ├── User.ts │ │ │ ├── UserListAdminResponse.ts │ │ │ ├── UserRole.enum.ts │ │ │ └── UserSettings.type.ts │ │ ├── pages │ │ │ └── LoginPage.vue │ │ ├── router │ │ │ └── index.ts │ │ ├── services │ │ │ ├── AuthenticationService.ts │ │ │ ├── PasswordService.ts │ │ │ ├── UserService.ts │ │ │ └── UserSettingsService.ts │ │ └── store │ │ │ ├── UserConstants.ts │ │ │ └── user.ts │ ├── services │ │ ├── InfiniteScrollBehaviour.ts │ │ ├── ScrollingActivationBehaviour.ts │ │ ├── model │ │ │ ├── Infinite.ts │ │ │ ├── InfiniteScrollable.ts │ │ │ └── ScrollActivable.ts │ │ └── notification │ │ │ ├── Notification.type.ts │ │ │ ├── NotificationCode.enum.ts │ │ │ ├── NotificationListener.ts │ │ │ ├── NotificationService.ts │ │ │ └── Severity.enum.ts │ ├── shims-vue.d.ts │ ├── store.ts │ ├── store │ │ └── sidenav │ │ │ ├── SidenavMutation.enum.ts │ │ │ └── sidenav.ts │ ├── teams │ │ ├── components │ │ │ ├── TeamEditor.vue │ │ │ └── TeamMembersInput.vue │ │ ├── model │ │ │ ├── Member.type.ts │ │ │ ├── MemberPending.enum.ts │ │ │ ├── Team.type.ts │ │ │ ├── TeamsSearchResponse.ts │ │ │ └── User.type.ts │ │ ├── pages │ │ │ └── TeamsPage.vue │ │ ├── router │ │ │ └── index.ts │ │ └── services │ │ │ ├── TeamMembers.service.ts │ │ │ └── Teams.service.ts │ └── techwatch │ │ ├── components │ │ └── newslist │ │ │ ├── NewsCard.vue │ │ │ ├── NewsList.vue │ │ │ └── model │ │ │ └── NewsView.ts │ │ ├── model │ │ ├── EventType.enum.ts │ │ ├── Feed.type.ts │ │ ├── Mark.enum.ts │ │ ├── News.type.ts │ │ ├── NewsSearchRequest.type.ts │ │ ├── NewsSearchResponse.type.ts │ │ ├── NewsState.type.ts │ │ └── Popularity.type.ts │ │ ├── pages │ │ ├── FeedsConfigPage.vue │ │ └── HomePage.vue │ │ ├── router │ │ └── index.ts │ │ ├── services │ │ ├── NewsService.ts │ │ ├── OpmlService.ts │ │ ├── PopularNewsService.ts │ │ └── TagsService.ts │ │ └── store │ │ ├── news.ts │ │ └── statistics │ │ ├── StatisticsConstants.ts │ │ └── statistics.ts ├── tailwind.config.js ├── tests │ └── unit │ │ ├── administration │ │ ├── component │ │ │ ├── FeedAdminTab.test.ts │ │ │ └── UserAdminTab.test.ts │ │ ├── page │ │ │ └── AdministrationPage.test.ts │ │ └── services │ │ │ └── FeedAdministrationService.test.ts │ │ ├── common │ │ └── components │ │ │ ├── BaywatchIcon.test.ts │ │ │ └── FileUploadWindow.test.ts │ │ ├── configuration │ │ ├── components │ │ │ ├── SettingsTabVue.test.ts │ │ │ ├── feedslist │ │ │ │ ├── FeedEditor.test.ts │ │ │ │ └── FeedsList.test.ts │ │ │ └── profile │ │ │ │ ├── ChangePasswordModal.test.ts │ │ │ │ └── ProfileTab.test.ts │ │ └── services │ │ │ └── FeedService.test.ts │ │ ├── layout │ │ └── components │ │ │ ├── TopNavigationBar.test.ts │ │ │ └── sidenav │ │ │ └── SideNav.test.ts │ │ ├── security │ │ └── pages │ │ │ └── LoginPage.test.ts │ │ ├── services │ │ └── notification │ │ │ └── NotificationService.test.ts │ │ ├── teams │ │ ├── components │ │ │ └── TeamEditor.test.ts │ │ └── pages │ │ │ └── TeamsPage.test.ts │ │ └── techwatch │ │ └── components │ │ └── newslist │ │ └── NewsList.test.ts ├── tsconfig.json ├── vite.config.ts ├── vitest.config.ts └── yarn.lock └── sonar-project.properties /.compose/grafana/provisioning/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | # config file version 2 | apiVersion: 1 3 | 4 | # list of datasources that should be deleted from the database 5 | #deleteDatasources: 6 | # - name: Prometheus 7 | # orgId: 1 8 | # - name: Loki 9 | # orgId: 1 10 | 11 | # list of datasources to insert/update depending 12 | # whats available in the database 13 | datasources: 14 | 15 | - name: Prometheus 16 | type: prometheus 17 | # access mode. direct or proxy. Required 18 | access: proxy 19 | url: http://prometheus:9090 20 | basicAuth: false 21 | isDefault: true 22 | jsonData: 23 | graphiteVersion: "1.1" 24 | tlsAuth: false 25 | tlsAuthWithCACert: false 26 | # json object of data that will be encrypted. 27 | version: 1 28 | editable: false 29 | 30 | - name: Loki 31 | type: loki 32 | access: proxy 33 | url: http://loki:3100 34 | jsonData: 35 | maxLines: 1000 36 | editable: false 37 | -------------------------------------------------------------------------------- /.compose/opentelemetry/exporter_logging.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | exporters: 4 | logging: 5 | verbosity: detailed 6 | -------------------------------------------------------------------------------- /.compose/opentelemetry/exporter_loki.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | exporters: 4 | loki: 5 | endpoint: 'http://loki:3100/loki/api/v1/push' 6 | -------------------------------------------------------------------------------- /.compose/opentelemetry/exporter_prometheus.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | exporters: 4 | prometheus: 5 | endpoint: 'opentelemetry:9091' 6 | send_timestamps: true 7 | enable_open_metrics: true 8 | -------------------------------------------------------------------------------- /.compose/opentelemetry/processor_attributes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | processors: 4 | attributes: 5 | actions: 6 | - action: insert 7 | key: loki.resource.labels 8 | value: 'application, container_id, container_name, container_image' 9 | -------------------------------------------------------------------------------- /.compose/opentelemetry/processor_batch.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | processors: 4 | batch: 5 | send_batch_size: 10000 6 | send_batch_max_size: 11000 7 | timeout: 10s 8 | -------------------------------------------------------------------------------- /.compose/opentelemetry/receiver_prometheus.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | receivers: 4 | prometheus: 5 | config: 6 | scrape_configs: 7 | - job_name: 'otel-collector' 8 | scrape_interval: 5s 9 | metrics_path: '/actuator/prometheus' 10 | static_configs: 11 | - targets: [baywatch:8081] 12 | labels: 13 | platform: 'devel' 14 | -------------------------------------------------------------------------------- /.compose/opentelemetry/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | service: 4 | pipelines: 5 | logs: 6 | receivers: [filelog/containers] 7 | processors: [attributes, batch] 8 | exporters: [logging, loki] 9 | metrics: 10 | receivers: [prometheus] 11 | processors: [batch] 12 | exporters: [prometheus] 13 | -------------------------------------------------------------------------------- /.docs/baywatch-capture-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marthym/baywatch/a6b3ac6954b3ffe75f6a3b1e7816d56e11a6ec7e/.docs/baywatch-capture-01.webp -------------------------------------------------------------------------------- /.docs/github-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marthym/baywatch/a6b3ac6954b3ffe75f6a3b1e7816d56e11a6ec7e/.docs/github-social.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/gitleaks.yml: -------------------------------------------------------------------------------- 1 | name: Gitleaks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | pull_request: 9 | types: [opened, synchronize, reopened] 10 | 11 | permissions: 12 | pull-requests: read 13 | 14 | jobs: 15 | gitleaks: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - uses: gitleaks/gitleaks-action@v2 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | GITLEAKS_VERSION: latest 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | *.db 4 | .attach_* 5 | .DS_Store 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | *.private.env.json 22 | 23 | ### VS Code ### 24 | .vscode/ 25 | 26 | -------------------------------------------------------------------------------- /.gitleaks.toml: -------------------------------------------------------------------------------- 1 | title = "Gitleaks Baywatch" 2 | [extend] 3 | useDefault = true 4 | 5 | [allowlist] 6 | description = "Ignore false positive" 7 | commits = [ 8 | "5a5bffb6a3fd2b0cb7d40849bd9f8c8d7f97dd5f" 9 | ] 10 | paths = [ 11 | "sandside/src/test" 12 | ] 13 | -------------------------------------------------------------------------------- /.run/All in sandside.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | -------------------------------------------------------------------------------- /.run/seaside serve.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | Baywatch 12 | 13 | 14 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /seaside/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | verbose: true, 3 | }; 4 | 5 | module.exports = config; 6 | 7 | // Or async function 8 | module.exports = async () => { 9 | return { 10 | verbose: true, 11 | preset: 'ts-jest', 12 | transform: { 13 | // ... 14 | // process `*.ts` files with `ts-jest` 15 | "^.+\\.tsx?$": "ts-jest" 16 | }, 17 | moduleNameMapper: { 18 | '^@/(.*)$': '/src/$1', 19 | }, 20 | testEnvironment: 'jsdom', 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /seaside/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marthym/baywatch/a6b3ac6954b3ffe75f6a3b1e7816d56e11a6ec7e/seaside/public/favicon.ico -------------------------------------------------------------------------------- /seaside/public/login.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marthym/baywatch/a6b3ac6954b3ffe75f6a3b1e7816d56e11a6ec7e/seaside/public/login.webp -------------------------------------------------------------------------------- /seaside/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /seaside/src/.env.production.local: -------------------------------------------------------------------------------- 1 | VITE_BW_VERSION=@project.version@ 2 | VITE_BW_COMMIT=@git.commit.id.abbrev@ 3 | -------------------------------------------------------------------------------- /seaside/src/administration/component/ConfigAdminTab.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /seaside/src/administration/model/Counter.type.ts: -------------------------------------------------------------------------------- 1 | export type Counter = { 2 | name: string, 3 | value: string, 4 | description: string, 5 | icon?: string 6 | }; 7 | 8 | export const NONE: Counter = { 9 | name: 'loading...', 10 | value: 'loading...', 11 | description: 'loading...' 12 | }; 13 | -------------------------------------------------------------------------------- /seaside/src/administration/model/RawFeed.type.ts: -------------------------------------------------------------------------------- 1 | import { ScrapingError } from '@/administration/model/ScraperError'; 2 | 3 | export type RawFeed = { 4 | _id: string 5 | name: string 6 | url: string 7 | icon: string 8 | description: string 9 | lastETag: string 10 | lastWatch: Date 11 | error?: ScrapingError 12 | } -------------------------------------------------------------------------------- /seaside/src/administration/model/ScraperError.ts: -------------------------------------------------------------------------------- 1 | export enum Level { 2 | WARNING = 'WARNING', 3 | SEVERE = 'SEVERE' 4 | } 5 | 6 | export type ScrapingError = { 7 | level: Level; 8 | since: string; 9 | lastTime: string; 10 | message: string; 11 | } -------------------------------------------------------------------------------- /seaside/src/administration/model/Statistics.type.ts: -------------------------------------------------------------------------------- 1 | import {Counter} from "@/administration/model/Counter.type"; 2 | 3 | export type Statistics = { 4 | news: Counter; 5 | feeds: Counter; 6 | users: Counter; 7 | scrap: Counter; 8 | } -------------------------------------------------------------------------------- /seaside/src/administration/model/UserView.ts: -------------------------------------------------------------------------------- 1 | import {User} from "@/security/model/User"; 2 | 3 | export type UserView = { 4 | data: User; 5 | isSelected: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /seaside/src/administration/services/StatisticsService.ts: -------------------------------------------------------------------------------- 1 | import {map, take} from "rxjs/operators"; 2 | import {Observable} from "rxjs"; 3 | import {Counter} from "@/administration/model/Counter.type"; 4 | import {send} from "@/common/services/GraphQLClient"; 5 | 6 | const STATISTICS_GQL_QUERY = `#graphql 7 | query Statistics { 8 | statistics{ name icon description value } 9 | }` 10 | 11 | export function get(): Observable { 12 | return send<{ statistics: Counter[] }>(STATISTICS_GQL_QUERY).pipe( 13 | map(data => data.data.statistics), 14 | take(1), 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /seaside/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marthym/baywatch/a6b3ac6954b3ffe75f6a3b1e7816d56e11a6ec7e/seaside/src/assets/logo.png -------------------------------------------------------------------------------- /seaside/src/assets/styles/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin "daisyui"; 4 | @plugin "daisyui/theme" { 5 | name: "baywatch"; 6 | default: true; 7 | prefersdark: true; 8 | color-scheme: dark; 9 | 10 | --color-primary: #ff5555; 11 | --color-primary-content: #ffeee9; 12 | --color-secondary: #e5e7eb; 13 | --color-secondary-content: #2d3748; 14 | --color-accent: #1ad5ff; 15 | --color-neutral: #374151; 16 | --color-base-100: #4B5563; 17 | --color-base-200: #1B263E; 18 | --color-base-300: #111827; 19 | --color-base-content: #d1d5db; 20 | --color-info: #2094f3; 21 | --color-success: #009485; 22 | --color-warning: #ff9900; 23 | --color-error: #d92b3a; 24 | --color-error-content: oklch(88% 0.062 18.334); 25 | } 26 | 27 | @layer components { 28 | .proposal-selected { 29 | @apply bg-base-content/20; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /seaside/src/common/components/ModalWindow.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /seaside/src/common/components/alertdialog/AlertDialog.types.ts: -------------------------------------------------------------------------------- 1 | import {Observable, Subject} from "rxjs"; 2 | import {InjectionKey} from "vue"; 3 | 4 | export const alertInjectionKey: InjectionKey = Symbol('IAlertDialog') as InjectionKey; 5 | 6 | export enum AlertType {INFO, CONFIRM_DELETE} 7 | 8 | export enum AlertResponse {OK, CONFIRM, CANCEL} 9 | 10 | export interface IAlertDialog { 11 | isFired: boolean; 12 | message: string; 13 | alertType: AlertType; 14 | confirmLabel: string; 15 | response?: Subject; 16 | 17 | fire(message: string, alertType: AlertType, confirmLabel?: string): Observable; 18 | 19 | close(response: AlertResponse): void; 20 | } 21 | -------------------------------------------------------------------------------- /seaside/src/common/components/notificationArea/NotificationView.ts: -------------------------------------------------------------------------------- 1 | import { Notification } from '@/services/notification/Notification.type'; 2 | 3 | export type NotificationView = { 4 | id: number; 5 | icon: string; 6 | raw: Notification; 7 | doneActions?: string; 8 | } -------------------------------------------------------------------------------- /seaside/src/common/components/smartTable/SmartTableData.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /seaside/src/common/components/smartTable/SmartTableLineAction.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | -------------------------------------------------------------------------------- /seaside/src/common/components/smartTable/SmartTableView.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SmartTableView { 2 | isSelected: boolean, 3 | isEditable: boolean, 4 | data?: T, 5 | } -------------------------------------------------------------------------------- /seaside/src/common/errors/BadRequestError.ts: -------------------------------------------------------------------------------- 1 | import { SandSideError } from '@/common/errors/SandSideError'; 2 | 3 | export class BadRequestError extends SandSideError { 4 | constructor(code: string, message: string) { 5 | super(code, message); 6 | } 7 | } -------------------------------------------------------------------------------- /seaside/src/common/errors/ForbiddenError.ts: -------------------------------------------------------------------------------- 1 | import {HttpStatusError} from "@/common/errors/HttpStatusError"; 2 | 3 | export class ForbiddenError extends HttpStatusError { 4 | constructor(message: string) { 5 | super(403, message); 6 | } 7 | } -------------------------------------------------------------------------------- /seaside/src/common/errors/HttpStatusError.ts: -------------------------------------------------------------------------------- 1 | import {SandSideError} from "@/common/errors/SandSideError"; 2 | 3 | export class HttpStatusError extends SandSideError { 4 | public httpStatus: number; 5 | 6 | constructor(httpStatus: number, message: string) { 7 | super(`SSHTTP${httpStatus}`, message); 8 | this.httpStatus = httpStatus; 9 | } 10 | } -------------------------------------------------------------------------------- /seaside/src/common/errors/SandSideError.ts: -------------------------------------------------------------------------------- 1 | export class SandSideError { 2 | public error: boolean; 3 | public code: string; 4 | public message: string; 5 | 6 | constructor(code: string, message: string) { 7 | this.error = true; 8 | this.code = code; 9 | this.message = message; 10 | } 11 | } -------------------------------------------------------------------------------- /seaside/src/common/errors/UnauthorizedError.ts: -------------------------------------------------------------------------------- 1 | import {HttpStatusError} from "@/common/errors/HttpStatusError"; 2 | 3 | export class UnauthorizedError extends HttpStatusError { 4 | public httpStatus: number; 5 | 6 | constructor(message: string) { 7 | super(401, message); 8 | } 9 | } -------------------------------------------------------------------------------- /seaside/src/common/errors/UnknownFetchError.ts: -------------------------------------------------------------------------------- 1 | import {SandSideError} from "@/common/errors/SandSideError"; 2 | 3 | export class UnknownFetchError extends SandSideError { 4 | 5 | constructor(message: string) { 6 | super("UKN0042", message); 7 | } 8 | } -------------------------------------------------------------------------------- /seaside/src/common/errors/ValidationError.ts: -------------------------------------------------------------------------------- 1 | import { SandSideError } from '@/common/errors/SandSideError'; 2 | 3 | export class ValidationError extends SandSideError { 4 | public properties: string[]; 5 | 6 | constructor(message: string, properties: string[]) { 7 | super('UKN0042', message); 8 | this.properties = properties; 9 | } 10 | } -------------------------------------------------------------------------------- /seaside/src/common/model/FeedCardView.type.ts: -------------------------------------------------------------------------------- 1 | type ScrapingError = { 2 | level: 'WARNING' | 'SEVERE'; 3 | since: Date; 4 | message: string; 5 | } 6 | 7 | export type FeedCardView = { 8 | _id: string; 9 | name: string; 10 | description: string; 11 | location: string; 12 | tags: string[]; 13 | icon?: URL; 14 | error?: ScrapingError; 15 | } -------------------------------------------------------------------------------- /seaside/src/common/model/Locale.type.ts: -------------------------------------------------------------------------------- 1 | export type Locale = 'fr-FR' | 'en-US'; -------------------------------------------------------------------------------- /seaside/src/common/model/NewsViewMode.ts: -------------------------------------------------------------------------------- 1 | export enum ViewMode { 2 | CARD = 'CARD', 3 | MAGAZINE = 'MAGAZINE', 4 | } 5 | 6 | export type ViewModeStrings = keyof typeof ViewMode; 7 | -------------------------------------------------------------------------------- /seaside/src/common/model/Page.ts: -------------------------------------------------------------------------------- 1 | import {Observable} from "rxjs"; 2 | 3 | export type Page = { 4 | currentPage: number; 5 | totalPage: number; 6 | data: Observable 7 | } -------------------------------------------------------------------------------- /seaside/src/common/model/SearchRequest.type.ts: -------------------------------------------------------------------------------- 1 | export interface SearchRequest { 2 | _p?: number 3 | _pp?: number 4 | _from?: number 5 | _to?: number 6 | _s?: string 7 | } -------------------------------------------------------------------------------- /seaside/src/common/services/RegexPattern.ts: -------------------------------------------------------------------------------- 1 | export const URL_PATTERN = /^(((https?):\/\/)(%[0-9A-Fa-f]{2}|[-()_.!~*';/?:@&=+$,A-Za-z0-9])+)([).!';/?:,][^\S\n\r])?$/; 2 | export const MAIL_PATTERN = /^[a-zA-Z0-9_+&*-]+(?:\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,7}$/; 3 | export const ULID_PATTERN = /^[A-Z]*(:[A-Z]{2}[0-7][0-9A-HJKMNP-TV-Z]{25})?$/; 4 | -------------------------------------------------------------------------------- /seaside/src/common/services/ReloadActionService.ts: -------------------------------------------------------------------------------- 1 | const EMPTY_CALLBACK: FunctionStringCallback = (context) => { 2 | console.warn(`no reload function for context ${context}!`); 3 | }; 4 | let reloadFunction: FunctionStringCallback = EMPTY_CALLBACK; 5 | 6 | export function actionServiceReload(context?: string): void { 7 | if (reloadFunction) { 8 | reloadFunction(context ?? ''); 9 | } 10 | } 11 | 12 | export function actionServiceUnregisterFunction(): void { 13 | reloadFunction = EMPTY_CALLBACK; 14 | } 15 | 16 | /** 17 | * Register the function call on reload 18 | * This allows others components to reload news list 19 | * 20 | * @param {FunctionStringCallback} apply The call function 21 | */ 22 | export function actionServiceRegisterFunction(apply: FunctionStringCallback): void { 23 | reloadFunction = apply; 24 | } 25 | -------------------------------------------------------------------------------- /seaside/src/configuration/model/Feed.type.ts: -------------------------------------------------------------------------------- 1 | type ScrapingError = { 2 | since: Date; 3 | message: string; 4 | } 5 | 6 | export type Feed = { 7 | _id: string; 8 | name: string; 9 | description: string; 10 | location: string; 11 | tags: string[]; 12 | icon?: URL; 13 | error: ScrapingError; 14 | } -------------------------------------------------------------------------------- /seaside/src/configuration/model/GraphQLScraper.type.ts: -------------------------------------------------------------------------------- 1 | export type AtomFeed = { 2 | title?: string, 3 | description?: string, 4 | icon?: string, 5 | } 6 | 7 | export type ScrapFeedHeaderResponse = { 8 | scrapFeedHeader: AtomFeed 9 | } -------------------------------------------------------------------------------- /seaside/src/configuration/model/SearchFeedsResponse.type.ts: -------------------------------------------------------------------------------- 1 | import {Feed} from "@/configuration/model/Feed.type"; 2 | import {SearchRequest} from "@/common/model/SearchRequest.type"; 3 | 4 | export type SearchFeedsResponse = { 5 | feedsSearch: { 6 | totalCount: number, 7 | entities: Feed[] 8 | } 9 | } 10 | 11 | export type SearchFeedsRequest = SearchRequest & { 12 | id?: string, 13 | name?: string, 14 | description?: string, 15 | tags?: string[] 16 | } -------------------------------------------------------------------------------- /seaside/src/configuration/pages/ConfigurationPage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /seaside/src/configuration/router/index.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router'; 2 | 3 | const ConfigurationPage = () => import('@/configuration/pages/ConfigurationPage.vue'); 4 | const FeedsList = () => import('@/configuration/components/feedslist/FeedsList.vue'); 5 | const ProfileTab = () => import('@/configuration/components/profile/ProfileTab.vue'); 6 | const SettingsTab = () => import('@/configuration/components/SettingsTab.vue'); 7 | 8 | export const routes: RouteRecordRaw[] = [ 9 | { 10 | path: '/config', component: ConfigurationPage, redirect: '/config/feeds', name: 'config', children: [ 11 | { path: 'feeds', component: FeedsList, name: 'config-feeds' }, 12 | { path: 'profile', component: ProfileTab, name: 'config-profile' }, 13 | { path: 'settings', component: SettingsTab, name: 'config-settings' }, 14 | ], 15 | meta: { requiresAuth: true }, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /seaside/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ConstantFilters = { 2 | PAGE: '_p', 3 | PER_PAGE: '_pp', 4 | FROM_PAGE: '_from' 5 | } 6 | 7 | export const ConstantHttpHeaders = { 8 | CONTENT_TYPE: 'Content-Type', 9 | X_TOTAL_COUNT: 'X-Total-Count', 10 | } 11 | 12 | export const ConstantMediaTypes = { 13 | JSON_UTF8: 'application/json; charset=utf-8', 14 | JSON_PATCH_UTF8: 'application/json-patch+json; charset=utf-8', 15 | } 16 | -------------------------------------------------------------------------------- /seaside/src/env.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | readonly VITE_API_BASE_URL: string 3 | readonly VITE_BW_VERSION: string 4 | readonly VITE_BW_COMMIT: string 5 | readonly VITE_GQL_ENDPOINT: string 6 | // more env variables... 7 | } 8 | 9 | interface ImportMeta { 10 | readonly env: ImportMetaEnv 11 | } -------------------------------------------------------------------------------- /seaside/src/layout/components/sidenav/SideNavOverlay.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 33 | -------------------------------------------------------------------------------- /seaside/src/layout/model/AtomFeed.type.ts: -------------------------------------------------------------------------------- 1 | export type AtomFeed = { 2 | id?: string, 3 | title?: string, 4 | description?: string, 5 | author?: string, 6 | link: string 7 | } 8 | -------------------------------------------------------------------------------- /seaside/src/layout/model/Feed.type.ts: -------------------------------------------------------------------------------- 1 | export type Feed = { 2 | _id: string 3 | _createdBy?: string, 4 | name?: string 5 | location: string 6 | lastWatch?: string 7 | tags?: string[] 8 | } 9 | -------------------------------------------------------------------------------- /seaside/src/layout/model/SearchResult.type.ts: -------------------------------------------------------------------------------- 1 | export enum SearchResultType { 2 | FEED = 'FEED', NEWS = 'NEWS' 3 | } 4 | 5 | export type SearchEntry = { 6 | id: string, 7 | _createdBy?: string, 8 | type: SearchResultType, 9 | name: string, 10 | url: string, 11 | } 12 | 13 | export type SearchIndexResponse = { 14 | searchIndex: SearchEntry[], 15 | } -------------------------------------------------------------------------------- /seaside/src/layout/services/ScraperService.ts: -------------------------------------------------------------------------------- 1 | import { Observable, throwError } from 'rxjs'; 2 | import { map, take } from 'rxjs/operators'; 3 | import { send } from '@/common/services/GraphQLClient'; 4 | import { URL_PATTERN } from '@/common/services/RegexPattern'; 5 | 6 | export class ScraperService { 7 | private static readonly SCRAP_SINGLE_NEWS_REQUEST = `#graphql 8 | mutation ScrapSingleNews($newsLink: URI!) { 9 | scrapSimpleNews(uri: $newsLink) 10 | }`; 11 | 12 | importStandaloneNews(link?: string): Observable { 13 | if (link === undefined) { 14 | return throwError(() => new Error('Link is mandatory !')); 15 | } else if (!URL_PATTERN.test(link)) { 16 | return throwError(() => new Error('Argument link must be a valid URL !')); 17 | } 18 | 19 | return send(ScraperService.SCRAP_SINGLE_NEWS_REQUEST, { newsLink: link }).pipe( 20 | take(1), 21 | map(() => undefined), 22 | ); 23 | } 24 | } 25 | 26 | export default new ScraperService(); -------------------------------------------------------------------------------- /seaside/src/locales/ClippedPage_en-US.ts: -------------------------------------------------------------------------------- 1 | import { en_US as home_page_en_US } from '@/locales/HomePage_en-US'; 2 | 3 | export const en_US = { 4 | ...home_page_en_US, 5 | }; 6 | -------------------------------------------------------------------------------- /seaside/src/locales/ClippedPage_fr-FR.ts: -------------------------------------------------------------------------------- 1 | import { fr_FR as home_page_fr_FR } from '@/locales/HomePage_fr-FR'; 2 | 3 | export const fr_FR = { 4 | ...home_page_fr_FR, 5 | }; 6 | -------------------------------------------------------------------------------- /seaside/src/locales/HomePage_en-US.ts: -------------------------------------------------------------------------------- 1 | export const en_US = { 2 | 'home.news.read': 'read', 3 | 'home.news.read.tooltip': 'mark as read', 4 | 'home.news.unread': 'unread', 5 | 'home.news.unread.tooltip': 'mark as unread', 6 | 'home.news.clip': 'clip', 7 | 'home.news.clip.tooltip': 'keep for later', 8 | 'home.news.share': 'share', 9 | }; 10 | -------------------------------------------------------------------------------- /seaside/src/locales/HomePage_fr-FR.ts: -------------------------------------------------------------------------------- 1 | export const fr_FR = { 2 | 'home.news.read': 'lu', 3 | 'home.news.read.tooltip': 'marquer comme lu', 4 | 'home.news.unread': 'non lu', 5 | 'home.news.unread.tooltip': 'marquer comme non lu', 6 | 'home.news.clip': 'garder', 7 | 'home.news.clip.tooltip': 'garder pour plus tard', 8 | 'home.news.share': 'partager', 9 | }; 10 | -------------------------------------------------------------------------------- /seaside/src/locales/LoginPage_en-US.ts: -------------------------------------------------------------------------------- 1 | export const en_US = { 2 | 'login.username': 'Username', 3 | 'login.password': 'Password', 4 | 'login.login': 'Log In', 5 | 'login.password.forget': 'Forget Password ?', 6 | }; 7 | -------------------------------------------------------------------------------- /seaside/src/locales/LoginPage_fr-FR.ts: -------------------------------------------------------------------------------- 1 | export const fr_FR = { 2 | 'login.username': 'Utilisateur', 3 | 'login.password': 'Mot de passe', 4 | 'login.login': 'Se Connecter', 5 | 'login.password.forget': 'Mot de passe oublié ?', 6 | }; 7 | -------------------------------------------------------------------------------- /seaside/src/locales/TeamsPage_en-US.ts: -------------------------------------------------------------------------------- 1 | import { smarttable_en_US } from '@/locales/components/smarttable_en-US'; 2 | 3 | export const en_US = { 4 | ...smarttable_en_US, 5 | 'teams.messages.teamJoined': 'You join the team {team} !', 6 | 'teams.messages.unknownError': 'Unknown error !', 7 | 'teams.messages.confirmLeaving': 'You will leave the team {team} definitively ?', 8 | 'teams.messages.leaveSuccessfully': 'You have left the team {team} ?', 9 | 'teams.messages.confirmDelete': 'Remove the team {team} ?', 10 | 'teams.messages.deletedSuccessfully': '{team} deleted successfully !', 11 | 'teams.messages.copiedUserClipboard': 'User ID copied on clipboard !', 12 | 'teams.leave': 'Leave', 13 | 'teams.header.name': 'name', 14 | 'teams.header.managers': 'managers', 15 | 'teams.header.topic': 'topic', 16 | }; -------------------------------------------------------------------------------- /seaside/src/locales/TeamsPage_fr-FR.ts: -------------------------------------------------------------------------------- 1 | import { smarttable_fr_FR } from '@/locales/components/smarttable_fr-FR'; 2 | 3 | export const fr_FR = { 4 | ...smarttable_fr_FR, 5 | 'teams.messages.teamJoined': 'Vous avez rejoint l’équipe {team} !', 6 | 'teams.messages.unknownError': 'Erreur inconnue !', 7 | 'teams.messages.confirmLeaving': 'Vous allez quitter l’équipe {team} définitivement ?', 8 | 'teams.messages.leaveSuccessfully': 'Vous avez quitter l’équipe {team} ?', 9 | 'teams.messages.confirmDelete': 'Êtes vous sûrs de vouloir supprimer l’équipe {team} ?', 10 | 'teams.messages.deletedSuccessfully': 'L’équipe {team} a été supprimé avec succès !', 11 | 'teams.messages.copiedUserClipboard': 'L’identifiant de l’utilisateur est copié dans le presse papier !', 12 | 'teams.leave': 'Quitter', 13 | 'teams.header.name': 'nom', 14 | 'teams.header.managers': 'capitaine', 15 | 'teams.header.topic': 'thème', 16 | }; -------------------------------------------------------------------------------- /seaside/src/locales/admin-feeds_en-US.ts: -------------------------------------------------------------------------------- 1 | import { en_US as admin_en_US } from '@/locales/admin_en-US'; 2 | import { backend_scraping_en_US } from '@/locales/backend/scraping_en-US'; 3 | 4 | export const en_US = { 5 | ...admin_en_US, 6 | ...backend_scraping_en_US, 7 | 'admin.feeds.confirm.feedsDeletion': 'Permanently delete the feed "{feed}" from the database ?' + 8 | '| Delete the {n} selected feeds from the database ?', 9 | 'admin.feeds.info.deletionMustContainsOneID': 'You must select at least one feed to delete!', 10 | 'admin.feeds.messages.feedDeletedSuccessfully': 'Feed {feed} successfully deleted!' + 11 | '| {n} feeds successfully deleted!', 12 | 'admin.feeds.messages.feedDeletionFailed': 'An error occurred while deleting the feed(s)!', 13 | 'admin.feeds.tab.feeds.list': 'Feed list', 14 | }; -------------------------------------------------------------------------------- /seaside/src/locales/admin-feeds_fr-FR.ts: -------------------------------------------------------------------------------- 1 | import { fr_FR as admin_fr_FR } from '@/locales/admin_fr-FR'; 2 | import { backend_scraping_fr_FR } from '@/locales/backend/scraping_fr-FR'; 3 | 4 | export const fr_FR = { 5 | ...admin_fr_FR, 6 | ...backend_scraping_fr_FR, 7 | 'admin.feeds.confirm.feedsDeletion': 'Supprimer le flux "{feed}" de la base de données définitivement ?' + 8 | '| Supprimer les {n} flux sélectionnés de la base de données ?', 9 | 'admin.feeds.info.deletionMustContainsOneID': 'Vous devez sélectionner au moins un flux à supprimer !', 10 | 'admin.feeds.messages.feedDeletedSuccessfully': 'Flux {feed} supprimé avec succès ! ' + 11 | '| {n} flux supprimés avec succès !', 12 | 'admin.feeds.messages.feedDeletionFailed': 'Une erreur s’est produite lors de la suppression de flux !', 13 | 'admin.feeds.tab.feeds.list': 'Liste des flux', 14 | }; -------------------------------------------------------------------------------- /seaside/src/locales/admin-stats_en-US.ts: -------------------------------------------------------------------------------- 1 | import { en_US as admin_en_US } from '@/locales/admin_en-US'; 2 | 3 | export const en_US = { 4 | ...admin_en_US, 5 | }; -------------------------------------------------------------------------------- /seaside/src/locales/admin-stats_fr-FR.ts: -------------------------------------------------------------------------------- 1 | import { fr_FR as admin_fr_FR } from '@/locales/admin_fr-FR'; 2 | 3 | export const fr_FR = { 4 | ...admin_fr_FR, 5 | }; -------------------------------------------------------------------------------- /seaside/src/locales/admin_en-US.ts: -------------------------------------------------------------------------------- 1 | export const en_US = { 2 | 'admin.tab.config': 'configuration', 3 | 'admin.tab.feeds': 'flux', 4 | 'admin.tab.stats': 'statistics', 5 | 'admin.tab.users': 'users', 6 | } 7 | -------------------------------------------------------------------------------- /seaside/src/locales/admin_fr-FR.ts: -------------------------------------------------------------------------------- 1 | export const fr_FR = { 2 | 'admin.tab.config': 'configuration', 3 | 'admin.tab.feeds': 'flux', 4 | 'admin.tab.stats': 'statistiques', 5 | 'admin.tab.users': 'utilisateurs', 6 | } 7 | -------------------------------------------------------------------------------- /seaside/src/locales/backend/scraping_en-US.ts: -------------------------------------------------------------------------------- 1 | export const backend_scraping_en_US = { 2 | 'sandside.scraping.default': 'Unknown error from feed scraping', 3 | 'sandside.scraping.parsing': 'Error on parsing feed flux. Will be fixed soon.', 4 | 'sandside.scraping.notFound': 'Feed not found.', 5 | 'sandside.scraping.needAccount': 'Feed expect credentials to be read', 6 | 'sandside.scraping.unsupported': 'Feed format unknown and not supported.', 7 | 'sandside.scraping.timeout': 'Flux request time out.', 8 | 'sandside.scraping.gone': 'Feed is not available for the moment.', 9 | 'sandside.scraping.unavailable': 'Feed does not exist or is not available anymore.', 10 | }; -------------------------------------------------------------------------------- /seaside/src/locales/backend/scraping_fr-FR.ts: -------------------------------------------------------------------------------- 1 | export const backend_scraping_fr_FR = { 2 | 'sandside.scraping.default': 'Erreur de recupération inconue', 3 | 'sandside.scraping.parsing': 'Le flux contient des erreurs, retentez plus tard', 4 | 'sandside.scraping.notFound': 'Flux not trouvé', 5 | 'sandside.scraping.needAccount': 'Le flux nécessite des permissions pour être lu', 6 | 'sandside.scraping.unsupported': 'Format du flux non supporté', 7 | 'sandside.scraping.timeout': 'Délai d’attente dépassé', 8 | 'sandside.scraping.gone': 'Le flux n’est pour l’instant plus accessible.', 9 | 'sandside.scraping.unavailable': 'Le flux n’existe pas ou plus', 10 | }; -------------------------------------------------------------------------------- /seaside/src/locales/components/fileuploadwindow_en-US.ts: -------------------------------------------------------------------------------- 1 | export const fileupload_en_US = { 2 | 'fileupload.title': 'Upload file', 3 | 'fileupload.form.action.choose': 'Choose', 4 | 'fileupload.form.action.choose.notice': 'or drag file here', 5 | 'fileupload.form.action.upload': 'upload', 6 | }; -------------------------------------------------------------------------------- /seaside/src/locales/components/fileuploadwindow_fr-FR.ts: -------------------------------------------------------------------------------- 1 | export const fileupload_fr_FR = { 2 | 'fileupload.title': 'Téléverser des fichiers', 3 | 'fileupload.form.action.choose': 'Sélectionner', 4 | 'fileupload.form.action.choose.notice': 'ou déplacer un fichier ici', 5 | 'fileupload.form.action.upload': 'Téléverser', 6 | }; -------------------------------------------------------------------------------- /seaside/src/locales/components/smarttable_en-US.ts: -------------------------------------------------------------------------------- 1 | export const smarttable_en_US = { 2 | 'smarttable.actions.add': 'add', 3 | 'smarttable.actions.import': 'import', 4 | 'smarttable.actions.export': 'export', 5 | 'smarttable.actions.leave': 'leave', 6 | 'smarttable.actions.delete': 'delete', 7 | 'smarttable.aria.usersList': 'user list', 8 | } -------------------------------------------------------------------------------- /seaside/src/locales/components/smarttable_fr-FR.ts: -------------------------------------------------------------------------------- 1 | export const smarttable_fr_FR = { 2 | 'smarttable.actions.add': 'ajouter', 3 | 'smarttable.actions.import': 'importer', 4 | 'smarttable.actions.export': 'exporter', 5 | 'smarttable.actions.leave': 'quitter', 6 | 'smarttable.actions.delete': 'supprimer', 7 | 'smarttable.aria.usersList': 'list des utilisateurs', 8 | } -------------------------------------------------------------------------------- /seaside/src/locales/components/taginput_en-US.ts: -------------------------------------------------------------------------------- 1 | export const taginput_en_US = { 2 | 'taginput.placeholder': 'add a tag...', 3 | } -------------------------------------------------------------------------------- /seaside/src/locales/components/taginput_fr-FR.ts: -------------------------------------------------------------------------------- 1 | export const taginput_fr_FR = { 2 | 'taginput.placeholder': 'ajouter un tag...', 3 | } -------------------------------------------------------------------------------- /seaside/src/locales/config-settings_en-US.ts: -------------------------------------------------------------------------------- 1 | import { en_US as config_en_US } from '@/locales/config_en-US'; 2 | 3 | export const en_US = { 4 | ...config_en_US, 5 | 'config.settings.form.preferredLocale': 'choose your preferred language', 6 | 'config.settings.form.newsView': 'choose news view mode :', 7 | 'config.settings.form.autoread.label': 'news auto read', 8 | 'config.settings.form.autoread.alt': 'In the article list, ' + 9 | 'autoread marks articles as read as soon as you move on to the next one.', 10 | 'config.settings.form.action.save': 'Save', 11 | 'config.settings.messages.settingsUpdateSuccessfully': 'Settings updated successfully', 12 | 'config.settings.messages.unableToUpdate': 'Unable to update settings', 13 | }; 14 | -------------------------------------------------------------------------------- /seaside/src/locales/config-settings_fr-FR.ts: -------------------------------------------------------------------------------- 1 | import { fr_FR as config_fr_FR } from '@/locales/config_fr-FR'; 2 | 3 | export const fr_FR = { 4 | ...config_fr_FR, 5 | 'config.settings.form.preferredLocale': 'choisissez votre langue préférée :', 6 | 'config.settings.form.newsView': 'choisissez le mode d’affichage des actualités :', 7 | 'config.settings.form.autoread.label': 'lecture automatique des articles', 8 | 'config.settings.form.autoread.alt': 'Dans la liste des articles, ' + 9 | 'l’autoread marque les articles comme lu dés que vous passez au suivant.', 10 | 'config.settings.form.action.save': 'enregistrer', 11 | 'config.settings.messages.settingsUpdateSuccessfully': 'Paramètres enregistrés avec succès.', 12 | 'config.settings.messages.unableToUpdate': 'Erreur lors de l’enregistrement des paramètres !', 13 | }; 14 | -------------------------------------------------------------------------------- /seaside/src/locales/config_en-US.ts: -------------------------------------------------------------------------------- 1 | export const en_US = { 2 | 'config.tab.feeds': 'feeds', 3 | 'config.tab.profile': 'profile', 4 | 'config.tab.settings': 'settings', 5 | } 6 | -------------------------------------------------------------------------------- /seaside/src/locales/config_fr-FR.ts: -------------------------------------------------------------------------------- 1 | export const fr_FR = { 2 | 'config.tab.feeds': 'flux', 3 | 'config.tab.profile': 'profil', 4 | 'config.tab.settings': 'paramètres', 5 | } 6 | -------------------------------------------------------------------------------- /seaside/src/main.ts: -------------------------------------------------------------------------------- 1 | import { store } from './store'; 2 | import App from './App.vue'; 3 | import '@/assets/styles/index.css'; 4 | import { createApp } from 'vue'; 5 | import { plugin as alertDialogPlugin } from '@/common/components/alertdialog/plugin'; 6 | import { router } from './router'; 7 | import { i18n } from '@/i18n'; 8 | 9 | createApp(App) 10 | .use(alertDialogPlugin) 11 | .use(router) 12 | .use(store) 13 | .use(i18n) 14 | .mount('#app'); 15 | -------------------------------------------------------------------------------- /seaside/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouterOptions } from 'vue-router'; 2 | import { routes as adminRoutes } from '@/administration/router'; 3 | import { routes as configRoutes } from '@/configuration/router'; 4 | import { routes as teamsRoutes } from '@/teams/router'; 5 | import { routes as techwatchRoutes } from '@/techwatch/router'; 6 | import { requireAuthNavGuard, routes as securityRoutes } from '@/security/router'; 7 | import { lazyloadTranslations } from '@/i18n'; 8 | 9 | export const router = createRouter({ 10 | history: createWebHashHistory(), 11 | routes: [ 12 | ...adminRoutes, 13 | ...teamsRoutes, 14 | ...configRoutes, 15 | ...techwatchRoutes, 16 | ...securityRoutes, 17 | { path: '/:catchAll(.*)*', redirect: '/news' }, 18 | ], 19 | } as RouterOptions); 20 | 21 | router.beforeEach(requireAuthNavGuard); 22 | router.beforeEach(lazyloadTranslations); 23 | -------------------------------------------------------------------------------- /seaside/src/security/model/PasswordEvaluation.type.ts: -------------------------------------------------------------------------------- 1 | export type PasswordEvaluation = { 2 | isSecure: boolean, 3 | entropy: number, 4 | message: string, 5 | }; -------------------------------------------------------------------------------- /seaside/src/security/model/Session.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@/security/model/User'; 2 | import { UserSettings } from '@/security/model/UserSettings.type'; 3 | 4 | export type Session = { 5 | user: User; 6 | settings: UserSettings, 7 | maxAge: number; 8 | } -------------------------------------------------------------------------------- /seaside/src/security/model/User.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | _id?: string; 3 | _createdAt?: string; 4 | _loginAt?: string; 5 | _loginIP?: string; 6 | login: string; 7 | name: string; 8 | mail: string; 9 | roles: string[]; 10 | password?: string; 11 | } 12 | 13 | export const ANONYMOUS: User = { 14 | login: '', 15 | name: 'Anonymous', 16 | mail: '', 17 | roles: [], 18 | }; -------------------------------------------------------------------------------- /seaside/src/security/model/UserListAdminResponse.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@/security/model/User'; 2 | 3 | type UserPage = { 4 | totalCount: number 5 | entities: User[] 6 | } 7 | export type UserListAdminResponse = { 8 | userSearch?: UserPage 9 | } 10 | -------------------------------------------------------------------------------- /seaside/src/security/model/UserRole.enum.ts: -------------------------------------------------------------------------------- 1 | export enum UserRole { 2 | SYSTEM = "SYSTEM", 3 | ADMIN = "ADMIN", 4 | MANAGER = "MANAGER", 5 | USER = "USER", 6 | ANONYMOUS = "ANONYMOUS" 7 | } -------------------------------------------------------------------------------- /seaside/src/security/model/UserSettings.type.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from '@/common/model/Locale.type'; 2 | import { ViewMode } from '@/common/model/NewsViewMode'; 3 | 4 | export type UserSettings = { 5 | preferredLocale: Locale, 6 | autoread: boolean, 7 | newsViewMode: ViewMode, 8 | } -------------------------------------------------------------------------------- /seaside/src/services/model/Infinite.ts: -------------------------------------------------------------------------------- 1 | import {Observable} from "rxjs"; 2 | 3 | export type Infinite = { 4 | total: number; 5 | data: Observable 6 | } -------------------------------------------------------------------------------- /seaside/src/services/model/InfiniteScrollable.ts: -------------------------------------------------------------------------------- 1 | import {Observable} from "rxjs"; 2 | 3 | export default interface InfiniteScrollable { 4 | loadNextPage(): Observable; 5 | } -------------------------------------------------------------------------------- /seaside/src/services/model/ScrollActivable.ts: -------------------------------------------------------------------------------- 1 | export default interface ScrollActivable { 2 | onScrollActivation(incr: number): Element; 3 | } -------------------------------------------------------------------------------- /seaside/src/services/notification/Notification.type.ts: -------------------------------------------------------------------------------- 1 | import {Severity} from "@/services/notification/Severity.enum"; 2 | import {NotificationCode} from "@/services/notification/NotificationCode.enum"; 3 | 4 | export type Notification = { 5 | code: NotificationCode; 6 | severity: Severity; 7 | title?: string; 8 | message: string; 9 | delay?: number; 10 | actions?: string; 11 | target?: string; 12 | } -------------------------------------------------------------------------------- /seaside/src/services/notification/NotificationCode.enum.ts: -------------------------------------------------------------------------------- 1 | export enum NotificationCode { 2 | UNAUTHENTICATED = 'UNAUTHENTICATED', 3 | UNAUTHORIZED = 'UNAUTHORIZED', 4 | OK = 'OK', 5 | ERROR = 'ERROR', 6 | NEWS_ADD = 'NEWS_ADD', 7 | } -------------------------------------------------------------------------------- /seaside/src/services/notification/NotificationListener.ts: -------------------------------------------------------------------------------- 1 | import {Notification} from "@/services/notification/Notification.type"; 2 | 3 | /** 4 | * Must be implemented to interact with {@link Notification} evolutions 5 | */ 6 | export default interface NotificationListener { 7 | /** 8 | * Executed when new {@link Notification} enter the stack 9 | * @param notif The new {@link Notification} 10 | */ 11 | onPushNotification(notif: Notification): void; 12 | 13 | /** 14 | * Executed when {@link Notification} go out of the stack 15 | * @param notif The {@link Notification} going out 16 | */ 17 | onPopNotification(notif: Notification): void; 18 | } -------------------------------------------------------------------------------- /seaside/src/services/notification/Severity.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Severity { 2 | error = 'error', 3 | warning = 'warning', 4 | info = 'info', 5 | notice = 'notice', 6 | } -------------------------------------------------------------------------------- /seaside/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | import { IAlertDialog } from '@/common/components/alertdialog/AlertDialog.types'; 2 | import 'vue-router'; 3 | 4 | declare module '*.vue' { 5 | import Vue from 'vue'; 6 | export default Vue; 7 | } 8 | 9 | declare module '@vue/runtime-core' { 10 | export interface ComponentCustomProperties { 11 | $alert: IAlertDialog; 12 | } 13 | } 14 | 15 | declare module 'vue-router' { 16 | interface RouteMeta { 17 | requiresAuth?: boolean; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /seaside/src/store.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, createStore } from 'vuex'; 2 | import { sidenav, SidenavState } from '@/store/sidenav/sidenav'; 3 | import { statistics, StatisticsState } from '@/techwatch/store/statistics/statistics'; 4 | import { user, UserState } from '@/security/store/user'; 5 | import { news } from '@/techwatch/store/news'; 6 | import { NewsStore } from '@/common/model/store/NewsStore.type'; 7 | 8 | 9 | type State = { 10 | news: NewsStore 11 | sidenav: SidenavState 12 | statistics: StatisticsState 13 | user: UserState 14 | } 15 | 16 | const debug = process.env.NODE_ENV !== 'production'; 17 | 18 | export const store = createStore({ 19 | modules: { 20 | news, 21 | sidenav, 22 | statistics, 23 | user, 24 | }, 25 | strict: debug, 26 | plugins: debug ? [createLogger()] : [], 27 | }); 28 | -------------------------------------------------------------------------------- /seaside/src/store/sidenav/SidenavMutation.enum.ts: -------------------------------------------------------------------------------- 1 | export enum SidenavMutation { 2 | TOGGLE = 'sidenav/toggleSidenav', 3 | OPEN_CREATE_ACCOUNT_MUTATION = 'user/openCreateAccount', 4 | } -------------------------------------------------------------------------------- /seaside/src/store/sidenav/sidenav.ts: -------------------------------------------------------------------------------- 1 | export type SidenavState = { 2 | open: boolean; 3 | } 4 | 5 | const state = (): SidenavState => ({ 6 | open: false, 7 | }); 8 | 9 | // getters 10 | const getters = {}; 11 | 12 | // actions 13 | const actions = {}; 14 | 15 | // mutations 16 | const mutations = { 17 | toggleSidenav(state: SidenavState): void { 18 | state.open = !state.open; 19 | }, 20 | }; 21 | 22 | export const sidenav = { 23 | namespaced: true, 24 | state, 25 | getters, 26 | actions, 27 | mutations, 28 | }; -------------------------------------------------------------------------------- /seaside/src/teams/model/Member.type.ts: -------------------------------------------------------------------------------- 1 | import {MemberPending} from "@/teams/model/MemberPending.enum"; 2 | import {User} from "@/teams/model/User.type"; 3 | 4 | export type Member = { 5 | _id: string, 6 | _user: User, 7 | pending: MemberPending, 8 | } -------------------------------------------------------------------------------- /seaside/src/teams/model/MemberPending.enum.ts: -------------------------------------------------------------------------------- 1 | export enum MemberPending { 2 | MANAGER = 'MANAGER', 3 | USER = 'USER', 4 | NONE = 'NONE', 5 | } -------------------------------------------------------------------------------- /seaside/src/teams/model/Team.type.ts: -------------------------------------------------------------------------------- 1 | import {User} from "@/teams/model/User.type"; 2 | import {MemberPending} from "@/teams/model/MemberPending.enum"; 3 | 4 | export type Team = { 5 | _id: string, 6 | _createdAt: string, 7 | _createdBy: string, 8 | _managers: User[], 9 | _me: { pending: MemberPending } 10 | name: string, 11 | topic: string, 12 | } -------------------------------------------------------------------------------- /seaside/src/teams/model/TeamsSearchResponse.ts: -------------------------------------------------------------------------------- 1 | import {Team} from "@/teams/model/Team.type"; 2 | 3 | export type TeamsSearchResponse = { 4 | teamsSearch: { 5 | totalCount: number 6 | entities: Team[] 7 | } 8 | } -------------------------------------------------------------------------------- /seaside/src/teams/model/User.type.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | _id: string, 3 | name: string, 4 | login: string, 5 | roles: string[], 6 | } -------------------------------------------------------------------------------- /seaside/src/teams/router/index.ts: -------------------------------------------------------------------------------- 1 | import {RouteRecordRaw} from "vue-router"; 2 | 3 | // @ts-ignore 4 | const TeamsPage = () => import('@/teams/pages/TeamsPage.vue'); 5 | 6 | export const routes: RouteRecordRaw[] = [ 7 | {path: '/teams', component: TeamsPage, name: 'TeamsPage'}, 8 | ]; 9 | -------------------------------------------------------------------------------- /seaside/src/techwatch/components/newslist/model/NewsView.ts: -------------------------------------------------------------------------------- 1 | import {News} from "@/techwatch/model/News.type"; 2 | 3 | export type NewsView = { 4 | data: News; 5 | isActive: boolean; 6 | 7 | sizes: string; 8 | srcset: string; 9 | 10 | /** 11 | * True if the user has explicitly mark the card. 12 | * The card does not change mark when change active 13 | */ 14 | keepMark: boolean; 15 | } -------------------------------------------------------------------------------- /seaside/src/techwatch/model/EventType.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EventType { 2 | NEWS_ADD = 'newsAdd', 3 | NEWS_UPDATE = 'newsUpdate', 4 | USER_NOTIFICATION = 'userNotification', 5 | } 6 | -------------------------------------------------------------------------------- /seaside/src/techwatch/model/Feed.type.ts: -------------------------------------------------------------------------------- 1 | export type Feed = { 2 | _id: string 3 | name?: string 4 | url: string 5 | tags?: string[] 6 | } 7 | -------------------------------------------------------------------------------- /seaside/src/techwatch/model/Mark.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Mark { 2 | READ = "read", 3 | SHARED = "shared", 4 | KEEP = "keep", 5 | } -------------------------------------------------------------------------------- /seaside/src/techwatch/model/News.type.ts: -------------------------------------------------------------------------------- 1 | import {NewsState} from "@/techwatch/model/NewsState.type"; 2 | import {Feed} from "@/techwatch/model/Feed.type"; 3 | import {Popularity} from "@/techwatch/model/Popularity.type"; 4 | 5 | export type News = { 6 | id: string 7 | title: string 8 | image: string 9 | imgm: string 10 | imgd: string 11 | description: string 12 | publication: string 13 | link: string 14 | feeds: Feed[] 15 | tags?: string 16 | state: NewsState 17 | popularity?: Popularity 18 | } 19 | -------------------------------------------------------------------------------- /seaside/src/techwatch/model/NewsSearchRequest.type.ts: -------------------------------------------------------------------------------- 1 | import {SearchRequest} from "@/common/model/SearchRequest.type"; 2 | 3 | export interface NewsSearchRequest extends SearchRequest { 4 | id?: string 5 | title?: string 6 | description?: string 7 | publication?: string 8 | feeds?: string[] 9 | tags?: string[] 10 | read?: boolean 11 | shared?: boolean 12 | popular?: boolean 13 | keep?: boolean 14 | } 15 | -------------------------------------------------------------------------------- /seaside/src/techwatch/model/NewsSearchResponse.type.ts: -------------------------------------------------------------------------------- 1 | import {News} from "@/techwatch/model/News.type"; 2 | 3 | export type NewsSearchResponse = { 4 | newsSearch?: { 5 | totalCount: number 6 | entities: News[] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /seaside/src/techwatch/model/NewsState.type.ts: -------------------------------------------------------------------------------- 1 | export type NewsState = { 2 | read: boolean; 3 | shared: boolean; 4 | keep: boolean; 5 | } -------------------------------------------------------------------------------- /seaside/src/techwatch/model/Popularity.type.ts: -------------------------------------------------------------------------------- 1 | export type Popularity = { 2 | score: number; 3 | fans: string[]; 4 | } 5 | -------------------------------------------------------------------------------- /seaside/src/techwatch/pages/FeedsConfigPage.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /seaside/src/techwatch/router/index.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router'; 2 | 3 | const HomePage = () => import('@/techwatch/pages/HomePage.vue'); 4 | 5 | export const routes: RouteRecordRaw[] = [ 6 | { path: '/news', component: HomePage, name: 'HomePage' }, 7 | { path: '/clipped', component: HomePage, name: 'ClippedPage' }, 8 | ]; 9 | -------------------------------------------------------------------------------- /seaside/src/techwatch/services/OpmlService.ts: -------------------------------------------------------------------------------- 1 | import {Observable, of} from "rxjs"; 2 | import rest from "@/common/services/RestWrapper"; 3 | import {switchMap, take} from "rxjs/operators"; 4 | import {HttpStatusError} from "@/common/errors/HttpStatusError"; 5 | 6 | export class OpmlService { 7 | upload(opml: File): Observable { 8 | const data = new FormData(); 9 | data.append('opml', opml); 10 | return rest.post('/opml/import', data).pipe( 11 | switchMap(response => { 12 | if (response.ok) { 13 | return of(true); 14 | } else { 15 | throw new HttpStatusError(response.status, 'Error while updating feed.'); 16 | } 17 | }), 18 | take(1) 19 | ); 20 | } 21 | } 22 | 23 | export default new OpmlService(); -------------------------------------------------------------------------------- /seaside/src/techwatch/services/PopularNewsService.ts: -------------------------------------------------------------------------------- 1 | import {Observable, switchMap} from "rxjs"; 2 | import {Popularity} from "@/techwatch/model/Popularity.type"; 3 | import rest from "@/common/services/RestWrapper"; 4 | import {take} from "rxjs/operators"; 5 | import {HttpStatusError} from "@/common/errors/HttpStatusError"; 6 | 7 | export class PopularNewsService { 8 | 9 | public get(ids: string[]): Observable { 10 | const query = new URLSearchParams(); 11 | ids.forEach(id => query.append('ids', id)); 12 | return rest.get(`/news/popularity?${query.toString()}`).pipe( 13 | switchMap(response => { 14 | if (response.ok) { 15 | return response.json(); 16 | } else { 17 | throw new HttpStatusError(response.status, `Error while getting news.`); 18 | } 19 | }), 20 | take(1) 21 | ); 22 | } 23 | } 24 | 25 | export default new PopularNewsService(); -------------------------------------------------------------------------------- /seaside/src/techwatch/services/TagsService.ts: -------------------------------------------------------------------------------- 1 | import { switchMap, take } from 'rxjs/operators'; 2 | import { HttpStatusError } from '@/common/errors/HttpStatusError'; 3 | import { from, Observable } from 'rxjs'; 4 | import rest from '@/common/services/RestWrapper'; 5 | 6 | export function tagsListAll(): Observable { 7 | return rest.get('/tags').pipe( 8 | switchMap(response => { 9 | if (response.ok) { 10 | const data: Observable = from(response.json()); 11 | return data; 12 | } else { 13 | throw new HttpStatusError(response.status, `Error while getting tags.`); 14 | } 15 | }), 16 | take(1), 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /seaside/src/techwatch/store/statistics/StatisticsConstants.ts: -------------------------------------------------------------------------------- 1 | export const NAMESPACE = 'statistics'; 2 | 3 | export const DECREMENT_UNREAD = 'decrementUnread'; 4 | export const INCREMENT_UNREAD = 'incrementUnread'; 5 | export const UPDATE = 'update'; 6 | export const RESET_UPDATED = 'resetUpdated'; 7 | export const FILTER = 'filter'; 8 | 9 | export const DECREMENT_UNREAD_MUTATION = `${NAMESPACE}/${DECREMENT_UNREAD}`; 10 | export const INCREMENT_UNREAD_MUTATION = `${NAMESPACE}/${INCREMENT_UNREAD}`; 11 | export const UPDATE_MUTATION = `${NAMESPACE}/${UPDATE}`; 12 | export const RESET_UPDATED_MUTATION = `${NAMESPACE}/${RESET_UPDATED}`; 13 | export const FILTER_MUTATION = `${NAMESPACE}/${FILTER}`; 14 | -------------------------------------------------------------------------------- /seaside/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './src/**/*.html', 4 | './src/**/*.vue', 5 | './src/**/*.jsx', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /seaside/tests/unit/common/components/BaywatchIcon.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import BaywatchIcon from '@/common/components/BaywatchIcon.vue'; 4 | 5 | describe('BaywatchIcon', () => { 6 | test('render Baywatch Icon', async () => { 7 | const wrapper = mount(BaywatchIcon); 8 | 9 | expect(wrapper.find('svg').exists()).toBe(true); 10 | }); 11 | }); -------------------------------------------------------------------------------- /seaside/tests/unit/common/components/FileUploadWindow.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import { createI18n } from 'vue-i18n'; 4 | import FileUploadWindow from '@/common/components/FileUploadWindow.vue'; 5 | 6 | describe('FileUploadWindow', () => { 7 | test('render file upload window', async () => { 8 | const i18n = createI18n({ 9 | legacy: false, 10 | missingWarn: false, 11 | messages: { 'en': {} }, 12 | }); 13 | 14 | const wrapper = mount(FileUploadWindow, { 15 | global: { 16 | plugins: [i18n], 17 | }, 18 | }); 19 | 20 | expect(wrapper.find('button').exists()).toBe(true); 21 | }); 22 | }); -------------------------------------------------------------------------------- /seaside/tests/unit/configuration/components/profile/ChangePasswordModal.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import ChangePasswordModal from '@/configuration/components/profile/ChangePasswordModal.vue'; 4 | import { createI18n } from 'vue-i18n'; 5 | import ModalWindow from '@/common/components/ModalWindow.vue'; 6 | 7 | describe('ChangePasswordModal', () => { 8 | test('renders a div', async () => { 9 | const i18n = createI18n({ 10 | legacy: false, 11 | missingWarn: false, 12 | messages: { 'en': {} }, 13 | }); 14 | const wrapper = mount(ChangePasswordModal, { global: { plugins: [i18n] } }); 15 | const modalWrapper = wrapper.findComponent(ModalWindow); 16 | expect(modalWrapper.exists()).toBe(true); 17 | expect(modalWrapper.isVisible()).toBe(false); 18 | 19 | await wrapper.setProps({ isOpen: true }); 20 | expect(modalWrapper.isVisible()).toBe(true); 21 | expect(modalWrapper.find('button').exists()).toBe(true); 22 | }); 23 | }); -------------------------------------------------------------------------------- /seaside/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env", 17 | "node", 18 | "vitest/globals" 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "src/**/*.ts", 34 | "src/**/*.tsx", 35 | "src/**/*.vue", 36 | "tests/**/*.ts", 37 | "tests/**/*.tsx" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /seaside/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from 'vitest/config'; 2 | import viteConfig from './vite.config'; 3 | 4 | export default defineConfig(configEnv => mergeConfig( 5 | viteConfig(configEnv), 6 | defineConfig({ 7 | test: { 8 | global: true, 9 | environment: 'jsdom', 10 | coverage: { 11 | reporter: ['text', 'lcov'], 12 | }, 13 | }, 14 | }), 15 | )); 16 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Marthym_baywatch 2 | sonar.javascript.lcov.reportPaths=seaside/coverage/lcov.info 3 | sonar.organization=ght1pc9kc-fr 4 | sonar.host.url=https://sonarcloud.io 5 | sonar.coverage.jacoco.xmlReportPaths=**/jacoco.xml 6 | sonar.coverage.exclusions=**/*.html,**/*.test.* 7 | sonar.exclusions=**/*.html,seaside/src/locales/**,seaside/dist/**,seaside/src/assets/styles/index.css 8 | sonar.java.binaries=sandside/target/classes/** 9 | sonar.java.libraries=sandside/target/dependency 10 | --------------------------------------------------------------------------------