🌦️ Welcome to the Weather Tracker project! Through this project, I’ve learned essential concepts such as secure session management, request filtering, and server-side rendering using Thymeleaf and Bootstrap. Weather Tracker lets users view, save, and organize weather information for various locations. It features session and cookie management with Redis, custom security filtering to handle authentication and authorization, and an intuitive user interface. Building this app has greatly enhanced my understanding of web security and architecture, and I look forward to leveraging this knowledge in future projects. |
---|
Visit the live version of our project here:
Weather Tracker.
If you are interested in learning more about dev stuff, you may find my article helpful, available here:
My blog
💡 My Goals (Beyond the Technical Task)
- SecurityFilterChain Imitation
- Application Context Management
- Integration Tests
- Session Management
- Servlets Structure
- Gain hands-on experience with
HttpClient
for making HTTP requests. - Explore Bootstrap and Thymeleaf to replace traditional CSS and JSP for UI design.
- Implement a custom
SecurityFilterChain
to pre-handle requests, manage authentication data during the request lifecycle, and control access to resources. - Develop a deeper understanding of session and cookie management by implementing a custom
Session
andSessionManager
. - Experiment with Redis as a session store, managing serialization and deserialization of session data.
- Build foundational knowledge of integration testing.
![]() Java |
![]() Docker |
Testcontainer |
![]() CI|CD |
Postgresql |
Hibernate |
IT |
Redis |
Bootstrap |
Maven |
Flyway |
Tomcat |
![]() JUnit |
HTML |
CSS |
-
☕ Java: The core language used for backend application logic and business services.
-
🐳 Docker: Containerization platform to package and deploy application components and dependencies consistently.
-
🧪 Testcontainers: A Java library that provides lightweight, disposable instances of databases and services for integration testing.
-
🔄 CI/CD: Continuous Integration and Continuous Deployment pipelines to automate building, testing, and deploying the application.
-
🐘 Postgresql: A robust, open-source relational database for storing and querying application data.
-
⚡ Redis: An in-memory data structure store used for caching and session management, optimizing data retrieval speed.
-
🎨 Bootstrap: A front-end framework for creating responsive, mobile-first UI designs with pre-built CSS components.
-
📦 Maven: Dependency management and build tool that automates compilation, packaging, and deployment of Java applications.
-
🦋 Flyway: Database migration tool for version control and migrations, ensuring consistent database schema management.
-
🖇 Hibernate: An ORM framework that maps Java objects to database tables, enabling efficient database access with minimal SQL.
-
🐱 Tomcat: Java servlet container for deploying and hosting Java web applications.
-
✅ JUnit: Testing framework for unit testing Java code, enabling automated test execution.
-
🌐 HTML & 🎨 CSS: Core web technologies for structuring and styling the application's front-end interface.
-
🍃 Thymeleaf: A Java templating engine used for server-side rendering of HTML pages, allowing dynamic content integration into views.
The custom security filter chain is composed of several filters, each responsible for specific security-related tasks.
This chain is initialized by the SecurityInitializer
class, which constructs and places the filter chain in the
application context.
-
DelegatingFilterProxy
: RegistersdefaultSecurityFilterChain
as a delegate object to preprocess incoming requests. -
DefaultSecurityFilterChain
: Implements the filter chain pattern, containing a set of filters responsible for web security.
Filters included in the chain:
CharacterEncodingFilter
: Sets the request and response encoding.SecurityContextHolderFilter
: Manages the security context, handling the current user’s authentication information.AuthenticationFilter
: Handles user authentication by verifying credentials and setting theAuthentication
object in theSecurityContext
.AnonymousAuthenticationFilter
: Creates an anonymous user if there’s no existingAuthentication
object.ExceptionTranslationFilter
: Translates authentication or authorization exceptions into HTTP responses.AuthorizationFilter
: Works withAuthorizationManager
to grant or deny access to requested resources.
-
SecurityContextHolderFilter: Initializes a
SecurityContext
that holds information about the current user, with deferred loading to improve performance. -
AuthenticationFilter: Manages the authentication logic, creating and setting an
Authentication
object in theSecurityContext
. -
AnonymousAuthenticationFilter: Sets up an anonymous user if the
SecurityContext
does not hold anAuthentication
object, enabling access for unauthenticated users. -
AuthorizationFilter: Works with the
AuthorizationManager
to grant or deny user rights to the requested resource based on user roles.
This implementation mimics the Spring Security filter chain using specific custom filters and configurations. The challenges and experience gained from this design offer valuable insights into security mechanisms and modular filter architecture.
The application context management is implemented through a custom Service Locator pattern using the BeanFactory
.
This approach digests configuration from the ApplicationContextConfiguration
class, which specifies the components (
beans) used across the application, and dynamically creates and injects dependencies.
-
BeanFactory
: Acts as the core of the Service Locator, responsible for registering and providing instances of beans as needed. It uses reflection to:- Identify methods annotated with
@Bean
inApplicationContextConfiguration
. - Instantiate beans based on their method definitions.
- Resolve and inject dependencies by identifying parameters of each method, ensuring each bean receives its required dependencies.
- Identify methods annotated with
-
ApplicationContextInitializer
: AServletContextListener
that initializes theBeanFactory
during application startup and registers it in the servlet context. This setup makes theBeanFactory
available to other components in the application.contextInitialized
: InvokesinjectFactoryBean()
to create theBeanFactory
withApplicationContextConfiguration
and store it in the servlet context.contextDestroyed
: Cleans up resources, such as theEntityManagerFactory
, and unregisters JDBC drivers upon application shutdown.
The ApplicationContextConfiguration
class defines various beans required across the application, including:
-
Database-Related Beans:
EntityManagerFactory
: Initializes the JPA entity manager for database access.Flyway
: Configures database migration using environment variables for database credentials.JedisPool
: Configures Redis connection pooling for efficient session management.
-
Service Beans: Services such as
UserService
,AuthenticationService
, andWeatherApiClient
are registered here, with each receiving necessary dependencies (e.g.,UserRepository
,PasswordEncoder
, andHttpClient
). -
Security Beans:
PasswordEncoder
(usingBCryptPasswordEncoder
) for secure password hashing.SecurityContextRepository
to manage user security contexts in HTTP sessions.AuthorizationManager
for role-based access control.
-
Utility Beans: Additional utility beans, such as
ObjectMapper
for JSON parsing andModelMapper
for object-to-object mapping, are also registered here.
The TemplateEngineInitializer
is a ServletContextListener
responsible for configuring the Thymeleaf template engine.
It:
- Sets up the
TemplateEngine
with HTML mode, UTF-8 encoding, and cache settings. - Uses a
WebApplicationTemplateResolver
to load templates from a specified path, set through application properties. - Stores the configured
TemplateEngine
in the servlet context for use in rendering dynamic HTML views.
The SecurityFilterInitializer
configures the custom security filter chain by retrieving the ServiceLocator
from the
servlet context and invoking the SecurityInitializer.initSecurityContext()
method to set up the security context.
The integration test suite focuses on verifying key functionality across the application's services and servlet components, ensuring they work as expected in a realistic environment with database and service dependencies. The tests simulate real-world scenarios, covering database transactions and remote requests.
-
ReflectionUtil
: This utility class leverages reflection to set private fields in objects, allowing tests to inject dependencies and mock objects where necessary. -
DbUtil
: Provides helper methods for common database-related tasks during tests.
TestContextInitializer
: This class initializes the application context specifically for testing, using a Testcontainers-managed PostgreSQL instance to mimic the production database environment. It sets up the database, configures theServiceLocator
with beans fromApplicationContextConfiguration
, and injects environment properties as needed.- Lifecycle Management: The
testPlanExecutionFinished
method ensures clean shutdown of database resources and containerized services after tests complete.
- Lifecycle Management: The
The servlet tests cover various application entry points for authentication, user management, and location services. Each servlet test:
- Mocks request and response objects using Mockito.
- Spies on the target servlet to control behavior and verify interactions.
-
WelcomeServletTest
:- Verifies redirection logic for authenticated and unauthenticated users.
- Uses
ReflectionUtil
to inject theUserService
and other dependencies.
-
WeatherServletTest
:- Tests retrieval and rendering of weather data for user locations.
- Uses
ArgumentCaptor
to capture and assert weather data set in the request attributes.
-
LocationServletTest
:- Tests location addition, deletion, and update operations for users.
- Includes parameterized tests for invalid data, ensuring robust error handling and coverage.
-
AuthenticationFilterTest
:- Mocks the
AuthenticationService
to simulate various authentication scenarios. - Tests both successful authentication and error handling, verifying request forwarding for failed logins.
- Mocks the
- Database Migrations: Each test class runs database migrations before tests and cleans up afterward using Flyway. This setup ensures the database is in a known state for every test.
- Testcontainers: The PostgreSQL container provided by Testcontainers runs as an isolated database instance, allowing tests to perform real SQL operations without affecting the production database.
The session management system is designed to securely handle user sessions across requests. It combines an in-memory session with Redis-backed storage for persistence, enabling efficient management of session data even in distributed environments.
-
CustomHttpSessionImpl
: This is the core session object, implementingCustomHttpSession
to provide session-specific attributes, authentication, and lifecycle methods. It supports:- Session Attributes: A
ConcurrentMap
stores session attributes, allowing data to be added, retrieved, or removed within the session. - Authentication Management: Sessions can hold an
Authentication
object for tracking the logged-in user, managed through methods such asgetAuthentication
,setAuthentication
, andisAuthenticated
. - Session Invalidation: The
invalidate()
method clears all session attributes, effectively logging out the user.
- Session Attributes: A
-
SessionManager
: Manages session creation, validation, storage, and retrieval. It leverages Redis for session persistence and manages cookies to track sessions across requests. Key functions include:- Session Creation and Persistence:
createSession
generates a new session, assigns it a UUID, and stores it in Redis with a specified timeout. - Session Retrieval and Validation:
getValidSession
retrieves a session using the session ID from the cookie, validates its timeout, and refreshes the last accessed time if valid. - Session Serialization/Deserialization: Uses
ObjectMapper
to convert sessions to JSON for storage in Redis and back into session objects upon retrieval. Custom serialization allows persisting complex attributes, such as authentication tokens. - Cookie Management: Creates and invalidates cookies to manage session IDs between requests, ensuring sessions
are secure with
HttpOnly
attributes and controlled expiration.
- Session Creation and Persistence:
-
Session Persistence in Redis:
- Redis is used to store session data, making it accessible across multiple server instances.
saveSessionToRedis
stores session data with an expiration time, whilegetSession
retrieves it for session continuity.
- Redis is used to store session data, making it accessible across multiple server instances.
-
Authentication and Authorization:
- Sessions hold
Authentication
objects, which allow the system to verify user identity. The session’sisAuthenticated
method checks if an authentication object is set and authenticated. - The session manager handles user login and logout by setting or removing the authentication data in sessions.
- Sessions hold
-
Session Timeout:
- Session timeout is managed by checking the last accessed time of each session. Sessions older than the configured timeout are considered invalid and automatically removed, enhancing security by limiting session duration.
-
Secure Cookie Handling:
- Cookies are created and updated to track sessions. Cookies are flagged as
HttpOnly
to protect against JavaScript access and set with a defined path and expiration, maintaining control over session lifecycles.
- Cookies are created and updated to track sessions. Cookies are flagged as
- Stateless Scalability: The Redis-backed session storage allows sessions to persist independently of application server restarts, ideal for scaling across distributed systems.
- Enhanced Security: The session system supports secure, time-limited sessions with Redis-backed persistence and
HttpOnly
cookies, reducing exposure to session hijacking.
-
AbstractThymeleafServlet: The base servlet class that extends
HttpServlet
and integrates Thymeleaf for server-side rendering. It provides:- Template Processing:
processTemplate
method loads and processes Thymeleaf templates, sending the output to the response. - DTO Parsing:
parseSimpleDto
method parses request parameters into data transfer objects (DTOs) with validation support. - Session and Authentication Management: Handles session retrieval and redirects based on user authentication status.
- Error Handling: Renders custom error pages for different exception scenarios.
- Template Processing:
-
WelcomeServlet: Handles requests to the welcome page. If the user is authenticated, it redirects to the weather page; otherwise, it displays the welcome template.
-
WeatherServlet: Retrieves weather data for authenticated users and renders it on a Thymeleaf template. It uses
UserService
to fetch user-specific weather data and sets it as a request attribute. -
LocationServlet: Manages user-specific location data, including adding, updating, and deleting locations. It supports:
- Method Overrides: Uses
_method
request parameter to determine if the request should be handled asPATCH
orDELETE
. - Location Management: Communicates with
WeatherApiClient
to search and update location data, with user locations managed throughUserService
.
- Method Overrides: Uses
-
Authorization Servlets: Handle user authentication flows, including login, signup, verification, and logout. These servlets extend
AbstractAuthServlet
for common functionality, such as checking user authentication status and managing redirects.- SignInServlet: Displays the login page or redirects authenticated users.
- SignUpServlet: Displays the signup page and handles user registration. It also sends a verification email upon successful registration.
- VerifyServlet: Verifies a user’s account based on a token.
- SignOutServlet: Manages logout by invalidating the user session and redirecting to the welcome page.
- SessionFilter: Checks each incoming request for a valid session. If no session is found, it creates a new one, and saves the session to Redis upon completion.
- DelegatingFilterProxy: Acts as a proxy to delegate security filters. It checks if a request matches configured security criteria and, if so, forwards it to the security filter chain.
- GlobalExceptionHandler: Catches and handles various application-specific exceptions, mapping each to an appropriate HTTP response or error page.
Servlets utilize Thymeleaf templates for dynamic content rendering, managed through AbstractThymeleafServlet
. The
template engine is configured centrally, allowing shared settings across templates.
Thymeleaf is a modern server-side Java templating engine designed to blend HTML templates with dynamic data in a readable and maintainable way. Unlike JSP, which embeds Java code directly within HTML, Thymeleaf allows you to define views using a natural templating approach. This means that Thymeleaf templates are valid HTML files, even before rendering, making them easier to design and visualize.
Thymeleaf enables dynamic content insertion through its powerful expression language, which avoids scriptlets entirely.
Instead, it uses simple, intuitive attributes (like th:text
, th:if
, and th:each
) to control how data and logic
appear within the HTML. This attribute-based approach keeps logic within the template but outside of the Java code,
allowing for a clean separation between the view layer and the application’s business logic.
When Thymeleaf processes a template, it evaluates these attributes on the server-side, injecting or modifying HTML based on dynamic data from the application. As a result, Thymeleaf combines server-side rendering with a rich set of features:
- Conditionals: Use attributes like
th:if
andth:unless
for conditional rendering. - Loops: The
th:each
attribute supports iteration over collections for displaying lists or tables. - Text Manipulation: Attributes like
th:text
andth:utext
replace specific HTML elements with dynamic text or raw HTML. - URL Management: The
th:href
andth:src
attributes help construct URLs dynamically based on application data.
Thymeleaf templates are processed on the server, producing fully rendered HTML that can then be sent to the client. This architecture keeps Thymeleaf highly compatible with MVC design patterns and makes it ideal for web applications that require a clear separation of concerns between the presentation and business logic layers.
HTML (HyperText Markup Language) is the standard markup language used to create and structure content on the web. It forms the backbone of any web page, defining the skeletal layout and assembly of various page elements. HTML uses a series of elements to encapsulate different types of content, ensuring web browsers know how to display each element correctly.
Elements are defined by tags. These elements can be nested, allowing for complex web page structures. Attributes within the tags provide additional settings or properties for the elements, such as setting a hyperlink’s destination with the href attribute in an tag.
HTML documents are essentially a hierarchy of elements, forming what is known as the DOM (Document Object Model), which scripts like JavaScript can manipulate to dynamically change the displayed content. This makes HTML not just a static skeleton but a dynamic foundation that interacts with user actions and scripting languages to create the rich, interactive web experiences familiar today.
By defining the content structure, HTML lays the groundwork for CSS and JavaScript, which respectively style and add interactivity to the web content, demonstrating HTML’s pivotal role in web development.
While HTML forms the hierarchical structure of web elements, CSS acts as their stylist, enabling the separation of presentation from content. This separation enhances accessibility and flexibility, allowing for detailed control over the layout, colors, and fonts without altering the HTML.
CSS operates by applying specific rules to targeted elements, reminiscent of inheritance in object-oriented programming (OOP), where properties are dynamically determined based on a set of cascading rules. These rules dictate how elements are styled and are applied based on criteria such as specificity, importance, and the source order of the CSS declarations.
Mastering CSS involves understanding its complexity and the nuances of how styles are applied, which can be a significant investment of time. My beginner attempting to use CSS cold only grasp the basics.
- Where:
SingletonSupplier<T>
,ServiceLocator
(inBeanFactory
) - Usage: Ensures that certain resources (e.g.,
ServiceLocator
and services) are created only once and then reused across the application, conserving resources and maintaining consistent behavior.
- Where:
BeanFactory
inServiceLocator
- Usage: Manages the lifecycle of beans (service instances) through reflection in
ApplicationContextConfiguration
. This supports dependency injection and centralizes service creation.
- Where:
ServiceLocator
interface andBeanFactory
implementation - Usage: Acts as a centralized registry for services (
UserService
,WeatherApiClient
, etc.), allowing classes to retrieve services without needing direct knowledge of their creation, reducing coupling.
- Where: Bean initialization in
ApplicationContextConfiguration
and injection viaServiceLocator
- Usage: Dependencies (e.g.,
UserService
,EmailService
,WeatherApiClient
) are injected rather than being instantiated directly, improving modularity and testability.
- Where:
SecurityFilterChain
and other filters (DelegatingFilterProxy
,AuthorizationFilter
, etc.) - Usage: Processes requests through a chain of filters, each responsible for a specific security aspect. Requests can pass through or stop at each filter based on conditions, providing modular and flexible security handling.
- Where:
AbstractThymeleafServlet
,AbstractAuthServlet
- Usage: Abstract classes define templates for servlet request handling and DTO parsing. Subclasses such as
WelcomeServlet
andWeatherServlet
implement specific actions but follow the defined template structure, ensuring consistency across servlets.
- Where:
DelegatingFilterProxy
- Usage: Acts as a wrapper to enhance security handling, conditionally applying security filters to requests matching specific criteria without altering the original behavior.
- Where: Classes like
UserRepository
,VerificationTokenDao
- Usage: Encapsulates data access operations, isolating persistence logic from business logic, which simplifies database access and improves testability.
- Where:
Redis
used inSessionManager
- Usage: Adapts Redis as a session store by creating a layer that bridges the differences between Redis’s API and the application’s session management requirements.
- Where:
ServletContextListener
classes such asTemplateEngineInitializer
,ApplicationContextInitializer
- Usage:
ServletContextListener
classes observe application context lifecycle events (initialization and destruction) to trigger setup tasks like initializing the template engine or database connections when the application starts.
- Where:
SecurityFilterChain
delegation viaDelegatingFilterProxy
- Usage:
DelegatingFilterProxy
acts as a stand-in for the security filter chain, controlling access and adding logging and validation, effectively securing and managing requests in a proxy structure.
To run this project locally with Docker Compose and PostgreSQL, use the provided docker-compose.yml
file.
- Environment Variables: Set up the required environment variables in
.env
and.docker-container.env
files (or as system variables) to manage configurations such as the database, mail service, Redis, and the Weather API key. - Docker and Docker Compose: Ensure Docker and Docker Compose are installed on your system.
Create and fill in the following files:
# Database Configuration
DB_DATA_HOME=$HOME/.local/share/db_data
WEATHER_TRACKER_DB_NAME=weather_tracker_db
WEATHER_TRACKER_DB_USER=weather_db_user
WEATHER_TRACKER_DB_PASSWORD=weather_db_password
# Redis Configuration (Local Settings)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_HOST_PORT=6386
# Mail Service Configuration
WEATHER_TRACKER_MAIL_SERVICE_CODE=<YOUR_MAIL_SERVICE_CODE>
WEATHER_TRACKER_MAIL_SERVICE_SENDER=<YOUR_MAIL_SERVICE_EMAIL>
# Weather API Key
WEATHER_TRACKER_REMOTE_API_KEY=<YOUR_API_KEY>
# Redis Configuration (Docker Network Settings)
REDIS_HOST=redis
REDIS_PORT=6379
-
Obtain an OpenWeather API Key:
- Register for a free API key from OpenWeather (with a 60 requests/minute limit) and set it in
.docker-container.env
asWEATHER_TRACKER_REMOTE_API_KEY
.
- Register for a free API key from OpenWeather (with a 60 requests/minute limit) and set it in
-
Fill in Mail Service Credentials:
- Update
.docker-container.env
with your mail service credentials, replacing<YOUR_MAIL_SERVICE_CODE>
and<YOUR_MAIL_SERVICE_EMAIL>
.
- Update
-
Start Docker Containers:
-
Open a terminal in the project root directory and run the following command to start the containers in detached mode:
docker-compose up -d
-
This command starts the PostgreSQL and Redis containers with the specified environment variables.
-
-
Access the Application
-
After starting the Docker containers and running the Tomcat server, open your browser and navigate to:
http://localhost:8080/api/v1/welcome
-
This URL should point to the welcome endpoint of your application, allowing you to verify that everything is set up correctly.
-
The deployment process for the Weather Tracker application is automated using GitHub Actions and follows a CI/CD pipeline. The process ensures that the application is built, tested, and deployed.
The pipeline consists of two main stages:
- Release Build Process
- Deployment to Remote Server
Each of these stages is executed through GitHub Actions workflows configured in release.yml and deploy.yml.
The release process is triggered when a tag is pushed to the repository.
-
Trigger: The process starts when a Git tag is pushed (on: push: tags: '*').
-
Build and Packaging:
- The source code is checked out from the repository.
- The JDK 21 (Eclipse Temurin) environment is set up.
- The Maven build tool is used to compile and package the application into a .war file.
- After the build, the .war file is copied to a designated directory.
- This .war file is uploaded as a build artifact to GitHub for further processing.
-
Release:
- Once the artifact is uploaded, the second stage of the job downloads the .war file.
- The ncipollo/release-action GitHub action is used to publish a release in the GitHub repository. The artifact is attached to the release, and this release is accessible for deployment purposes.
The deployment is handled through the deploy.yml file, which is triggered when the release process is completed (on: workflow_run).
- SSH into Remote Server: The workflow uses appleboy/ssh-action to remotely connect to the server where the application is hosted.
- Tomcat Restart: The running Tomcat server is stopped using the shutdown.sh script.
- Backup the Current WAR File: If an existing WAR file is found, it is backed up by renaming it (e.g., *.war.bak).
- Download Latest Release: The workflow dynamically retrieves the latest release from the GitHub repository using the GitHub API. The downloaded WAR file is placed in the Tomcat webapps directory.
- Set File Permissions: The file permissions are adjusted so that Tomcat can read and execute the new WAR file.
- Start Tomcat: The Tomcat server is restarted using the startup.sh script to deploy the new version of the application.
-
Front-End Server (Apache2): The application is hosted on a remote server where Apache2 acts as the front-end web server. Apache handles all incoming HTTP (port 80) and HTTPS (port 443) traffic and serves as a proxy to the Tomcat server. Apache is responsible for managing SSL certificates and encrypting traffic.
-
Reverse Proxy to Tomcat: Apache2 is configured to proxy all incoming requests for the subdomain (weather-tracker.ale-os.com) to a Tomcat server running in the background on the same machine. Apache forwards the requests to Tomcat, which is bound to localhost. Tomcat handles the application logic and serves the content back to Apache, which then returns the response to the client.
-
Tomcat Configuration: Tomcat runs as a background service on the remote server and is configured to serve the application via localhost. The WAR file is deployed in Tomcat's webapps directory, and Apache2 forwards requests to Tomcat using proxy directives. Tomcat does not handle SSL directly; instead, Apache takes care of encrypting the communication and proxies the traffic securely to Tomcat.
-
SSL Configuration: SSL certificates are managed by Apache2 using Let's Encrypt. All HTTPS traffic is terminated at Apache, and the requests are then proxied to Tomcat over unencrypted HTTP.
Several secrets are used within the CI/CD pipeline to ensure security:
- SSH Keys: The deployment process uses SSH keys (secrets.SSH_AWS) to connect securely to the remote server.
- GitHub Token: The release action uses a GitHub token (secrets.FOR_RELEASE_TOKEN) to create and upload releases.
- Username and Host: The username and host details for the remote server are stored securely in GitHub secrets ( secrets.USERNAME, secrets.REMOTE_HOST).
- WEATHER_TRACKER_REMOTE_API_KEY: API key for accessing the Weather API.
This CI/CD pipeline automates the build, release, and deployment process for the Tennis-Scoreboard application. Using GitHub Actions, Maven, and Tomcat, the system ensures that each new release is built and deployed to production with minimal manual intervention.
I am continuously looking to refine my understanding and implementation of programming. If you have insights, critiques, or advice—or if you wish to discuss any aspect of this project further—I warmly welcome your contributions. Please feel free to open an issue to share your thoughts.
I want to express my gratitude to
S. Zhukov, the author of the
technical
requirements.
Made with ☀️ and 🌧 by Aleos.