From 4393977389bb906ae452d1101295942b9968d51c Mon Sep 17 00:00:00 2001 From: masiton Date: Fri, 4 Jul 2025 21:24:12 +0200 Subject: [PATCH] Initial commit carried over from private repo. This is V2. --- .gitea/workflows/build.yml | 26 ++ .gitea/workflows/docker-build-and-push.yml | 23 ++ Directory.Packages.props | 12 + Dockerfile | 24 ++ LICENSE | 10 + NuGet.config | 15 + README.md | 129 ++++++++ assets/why.jpg | Bin 0 -> 171164 bytes .../Enums/HousekeepingMessageStates.cs | 9 + .../Enums/LoggingVerbosity.cs | 9 + .../Enums/PersistenceTypes.cs | 8 + .../MessengerApi.Configuration.csproj | 9 + .../Model/MessengerConfiguration.cs | 98 ++++++ .../Base/PersistenceConfiguration.cs | 9 + .../NpgPersistenceConfiguration.cs | 20 ++ .../SqlPersistenceConfiguration.cs | 20 ++ .../Parsers/CorsParser.cs | 11 + ...vironmentPersistenceConfigurationParser.cs | 25 ++ .../Parsers/HousekeepingMessageStateParser.cs | 12 + .../Parsers/LoggingVerbosityParser.cs | 12 + .../Parsers/PersistenceTypeParser.cs | 12 + .../Parsers/ProxiesParser.cs | 11 + .../Constants.EnvironmentVariables.cs | 20 ++ .../EnvironmentConfigurationSource.cs | 15 + .../IEnvironmentConfigurationSource.cs | 6 + .../Sources/IConfigurationSource.cs | 9 + .../IMessageParser.cs | 22 ++ .../MessageParser.cs | 50 +++ ...essengerApi.Contracts.MessageParser.csproj | 18 + .../Client/IMessengerClient.cs | 30 ++ .../Client/MessengerClient.cs | 150 +++++++++ code/MessengerApi.Contracts/Contact.cs | 9 + code/MessengerApi.Contracts/Credentials.cs | 15 + .../Messages/InboxMessage.cs | 20 ++ .../Messages/OutboxMessage.cs | 20 ++ .../MessengerApi.Contracts.csproj | 18 + .../Entities/IEntity.cs | 11 + .../Entities/Message.cs | 25 ++ .../Entities/User.cs | 15 + .../Entities/UserRoute.cs | 16 + .../MessengerApi.Db.Contracts.csproj | 9 + .../Repositories/IMessageRepository.cs | 9 + .../Repositories/IRepository.cs | 15 + .../Repositories/IUserRepository.cs | 9 + .../Repositories/IUserRouteRepository.cs | 17 + .../DesignTimeDbContextFactory.cs | 13 + .../MessengerApi.Db.Npg.Migrator.csproj | 21 ++ code/MessengerApi.Db.Npg.Migrator/Program.cs | 16 + .../MessengerApi.Db.Npg.csproj | 17 + .../MessengerNpgDbContext.cs | 31 ++ .../20250704170425_Initial.Designer.cs | 123 +++++++ .../Migrations/20250704170425_Initial.cs | 99 ++++++ .../MessengerNpgDbContextModelSnapshot.cs | 120 +++++++ .../DesignTimeDbContextFactory.cs | 12 + .../MessengerApi.Db.Sql.Migrator.csproj | 22 ++ code/MessengerApi.Db.Sql.Migrator/Program.cs | 16 + .../MessengerApi.Db.Sql.csproj | 21 ++ .../MessengerSqlDbContext.cs | 31 ++ .../20250704165018_Initial.Designer.cs | 125 +++++++ .../Migrations/20250704165018_Initial.cs | 100 ++++++ .../MessengerSqlDbContextModelSnapshot.cs | 122 +++++++ .../Converters/DateTimeAsUtcValueConverter.cs | 8 + code/MessengerApi.Db/MessengerApi.Db.csproj | 19 ++ code/MessengerApi.Db/MessengerDbContext.cs | 26 ++ .../Repositories/MessageRepository.cs | 23 ++ .../Repositories/Repository.cs | 26 ++ .../Repositories/UserRepository.cs | 18 + .../Repositories/UserRouteRepository.cs | 23 ++ code/MessengerApi.sln | 156 +++++++++ code/MessengerApi/.config/dotnet-tools.json | 13 + code/MessengerApi/Constants.cs | 7 + .../Contracts/Factories/IDbContextFactory.cs | 9 + .../Contracts/Models/Scoped/IUnitOfWork.cs | 15 + code/MessengerApi/Dockerfile | 8 + .../Factories/DbContextFactory.cs | 34 ++ code/MessengerApi/Factories/LoggerFactory.cs | 32 ++ code/MessengerApi/GlobalUsings.cs | 2 + .../CustomBearerAuthenticationHandler.cs | 81 +++++ .../Handlers/Endpoint/AckEndpointHandler.cs | 40 +++ .../Handlers/Endpoint/PeekEndpointHandler.cs | 35 ++ .../Endpoint/ReceiveEndpointHandler.cs | 43 +++ .../Handlers/Endpoint/SendEndpointHandler.cs | 60 ++++ .../Handlers/HousekeepingHandler.cs | 47 +++ .../MessengerApi/Handlers/UserSetupHandler.cs | 99 ++++++ code/MessengerApi/MessengerApi.csproj | 30 ++ code/MessengerApi/Models/CachedIdentity.cs | 13 + code/MessengerApi/Models/Http/AckRequest.cs | 7 + code/MessengerApi/Models/Http/SendRequest.cs | 13 + .../MessengerApi/Models/Http/VerifyRequest.cs | 7 + code/MessengerApi/Models/Scoped/Identity.cs | 9 + code/MessengerApi/Models/Scoped/Timing.cs | 7 + code/MessengerApi/Models/Scoped/UnitOfWork.cs | 33 ++ code/MessengerApi/Models/UserSetupItem.cs | 11 + code/MessengerApi/Program.cs | 313 ++++++++++++++++++ .../Properties/launchSettings.json | 28 ++ docker-compose.yml | 7 + 96 files changed, 3223 insertions(+) create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitea/workflows/docker-build-and-push.yml create mode 100644 Directory.Packages.props create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 NuGet.config create mode 100644 README.md create mode 100644 assets/why.jpg create mode 100644 code/MessengerApi.Configuration/Enums/HousekeepingMessageStates.cs create mode 100644 code/MessengerApi.Configuration/Enums/LoggingVerbosity.cs create mode 100644 code/MessengerApi.Configuration/Enums/PersistenceTypes.cs create mode 100644 code/MessengerApi.Configuration/MessengerApi.Configuration.csproj create mode 100644 code/MessengerApi.Configuration/Model/MessengerConfiguration.cs create mode 100644 code/MessengerApi.Configuration/Model/Persistence/Base/PersistenceConfiguration.cs create mode 100644 code/MessengerApi.Configuration/Model/Persistence/NpgPersistenceConfiguration.cs create mode 100644 code/MessengerApi.Configuration/Model/Persistence/SqlPersistenceConfiguration.cs create mode 100644 code/MessengerApi.Configuration/Parsers/CorsParser.cs create mode 100644 code/MessengerApi.Configuration/Parsers/EnvironmentPersistenceConfigurationParser.cs create mode 100644 code/MessengerApi.Configuration/Parsers/HousekeepingMessageStateParser.cs create mode 100644 code/MessengerApi.Configuration/Parsers/LoggingVerbosityParser.cs create mode 100644 code/MessengerApi.Configuration/Parsers/PersistenceTypeParser.cs create mode 100644 code/MessengerApi.Configuration/Parsers/ProxiesParser.cs create mode 100644 code/MessengerApi.Configuration/Sources/Environment/Constants.EnvironmentVariables.cs create mode 100644 code/MessengerApi.Configuration/Sources/Environment/EnvironmentConfigurationSource.cs create mode 100644 code/MessengerApi.Configuration/Sources/Environment/IEnvironmentConfigurationSource.cs create mode 100644 code/MessengerApi.Configuration/Sources/IConfigurationSource.cs create mode 100644 code/MessengerApi.Contracts.MessageParser/IMessageParser.cs create mode 100644 code/MessengerApi.Contracts.MessageParser/MessageParser.cs create mode 100644 code/MessengerApi.Contracts.MessageParser/MessengerApi.Contracts.MessageParser.csproj create mode 100644 code/MessengerApi.Contracts/Client/IMessengerClient.cs create mode 100644 code/MessengerApi.Contracts/Client/MessengerClient.cs create mode 100644 code/MessengerApi.Contracts/Contact.cs create mode 100644 code/MessengerApi.Contracts/Credentials.cs create mode 100644 code/MessengerApi.Contracts/Messages/InboxMessage.cs create mode 100644 code/MessengerApi.Contracts/Messages/OutboxMessage.cs create mode 100644 code/MessengerApi.Contracts/MessengerApi.Contracts.csproj create mode 100644 code/MessengerApi.Db.Contracts/Entities/IEntity.cs create mode 100644 code/MessengerApi.Db.Contracts/Entities/Message.cs create mode 100644 code/MessengerApi.Db.Contracts/Entities/User.cs create mode 100644 code/MessengerApi.Db.Contracts/Entities/UserRoute.cs create mode 100644 code/MessengerApi.Db.Contracts/MessengerApi.Db.Contracts.csproj create mode 100644 code/MessengerApi.Db.Contracts/Repositories/IMessageRepository.cs create mode 100644 code/MessengerApi.Db.Contracts/Repositories/IRepository.cs create mode 100644 code/MessengerApi.Db.Contracts/Repositories/IUserRepository.cs create mode 100644 code/MessengerApi.Db.Contracts/Repositories/IUserRouteRepository.cs create mode 100644 code/MessengerApi.Db.Npg.Migrator/DesignTimeDbContextFactory.cs create mode 100644 code/MessengerApi.Db.Npg.Migrator/MessengerApi.Db.Npg.Migrator.csproj create mode 100644 code/MessengerApi.Db.Npg.Migrator/Program.cs create mode 100644 code/MessengerApi.Db.Npg/MessengerApi.Db.Npg.csproj create mode 100644 code/MessengerApi.Db.Npg/MessengerNpgDbContext.cs create mode 100644 code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.Designer.cs create mode 100644 code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.cs create mode 100644 code/MessengerApi.Db.Npg/Migrations/MessengerNpgDbContextModelSnapshot.cs create mode 100644 code/MessengerApi.Db.Sql.Migrator/DesignTimeDbContextFactory.cs create mode 100644 code/MessengerApi.Db.Sql.Migrator/MessengerApi.Db.Sql.Migrator.csproj create mode 100644 code/MessengerApi.Db.Sql.Migrator/Program.cs create mode 100644 code/MessengerApi.Db.Sql/MessengerApi.Db.Sql.csproj create mode 100644 code/MessengerApi.Db.Sql/MessengerSqlDbContext.cs create mode 100644 code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.Designer.cs create mode 100644 code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.cs create mode 100644 code/MessengerApi.Db.Sql/Migrations/MessengerSqlDbContextModelSnapshot.cs create mode 100644 code/MessengerApi.Db/Converters/DateTimeAsUtcValueConverter.cs create mode 100644 code/MessengerApi.Db/MessengerApi.Db.csproj create mode 100644 code/MessengerApi.Db/MessengerDbContext.cs create mode 100644 code/MessengerApi.Db/Repositories/MessageRepository.cs create mode 100644 code/MessengerApi.Db/Repositories/Repository.cs create mode 100644 code/MessengerApi.Db/Repositories/UserRepository.cs create mode 100644 code/MessengerApi.Db/Repositories/UserRouteRepository.cs create mode 100644 code/MessengerApi.sln create mode 100644 code/MessengerApi/.config/dotnet-tools.json create mode 100644 code/MessengerApi/Constants.cs create mode 100644 code/MessengerApi/Contracts/Factories/IDbContextFactory.cs create mode 100644 code/MessengerApi/Contracts/Models/Scoped/IUnitOfWork.cs create mode 100644 code/MessengerApi/Dockerfile create mode 100644 code/MessengerApi/Factories/DbContextFactory.cs create mode 100644 code/MessengerApi/Factories/LoggerFactory.cs create mode 100644 code/MessengerApi/GlobalUsings.cs create mode 100644 code/MessengerApi/Handlers/CustomBearerAuthenticationHandler.cs create mode 100644 code/MessengerApi/Handlers/Endpoint/AckEndpointHandler.cs create mode 100644 code/MessengerApi/Handlers/Endpoint/PeekEndpointHandler.cs create mode 100644 code/MessengerApi/Handlers/Endpoint/ReceiveEndpointHandler.cs create mode 100644 code/MessengerApi/Handlers/Endpoint/SendEndpointHandler.cs create mode 100644 code/MessengerApi/Handlers/HousekeepingHandler.cs create mode 100644 code/MessengerApi/Handlers/UserSetupHandler.cs create mode 100644 code/MessengerApi/MessengerApi.csproj create mode 100644 code/MessengerApi/Models/CachedIdentity.cs create mode 100644 code/MessengerApi/Models/Http/AckRequest.cs create mode 100644 code/MessengerApi/Models/Http/SendRequest.cs create mode 100644 code/MessengerApi/Models/Http/VerifyRequest.cs create mode 100644 code/MessengerApi/Models/Scoped/Identity.cs create mode 100644 code/MessengerApi/Models/Scoped/Timing.cs create mode 100644 code/MessengerApi/Models/Scoped/UnitOfWork.cs create mode 100644 code/MessengerApi/Models/UserSetupItem.cs create mode 100644 code/MessengerApi/Program.cs create mode 100644 code/MessengerApi/Properties/launchSettings.json create mode 100644 docker-compose.yml diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..b550051 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,26 @@ +name: Build and Push Docker Image + +on: + push: + +jobs: + build: + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/dotnet/sdk:9.0 + steps: + - name: Install Node.js and dependencies + run: | + apt-get update + apt-get install -y curl gnupg + curl -fsSL https://deb.nodesource.com/setup_18.x | bash - + apt-get install -y nodejs git + + - name: Checkout + uses: actions/checkout@v3 + + - name: Restore dependencies + run: dotnet restore ./code/MessengerApi/MessengerApi.csproj + + - name: Build project + run: dotnet build ./code/MessengerApi/MessengerApi.csproj -c Release \ No newline at end of file diff --git a/.gitea/workflows/docker-build-and-push.yml b/.gitea/workflows/docker-build-and-push.yml new file mode 100644 index 0000000..66f00f1 --- /dev/null +++ b/.gitea/workflows/docker-build-and-push.yml @@ -0,0 +1,23 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + +jobs: + docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v3 + + - name: Docker login + run: echo "${{ secrets.DOCKER_TOKEN }}" | docker login https://gitea.masita.net -u mc --password-stdin + + - name: Build and push Docker image + run: | + IMAGE=gitea.masita.net/mc/messengerapi:latest + docker build -t $IMAGE . + docker push $IMAGE \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..d7f66d9 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..63b948b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# Base image +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Copy and restore with custom config +COPY NuGet.config ./ +COPY Directory.Packages.props ./ +COPY code/ ./code/ + +WORKDIR /src/code/MessengerApi +RUN dotnet restore MessengerApi.csproj + +# Build and publish +RUN dotnet publish MessengerApi.csproj -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=build /app/publish . + +ENTRYPOINT ["dotnet", "MessengerApi.dll"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cde4ac6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,10 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..c197527 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..da3f13d --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# messengerapi + +[!["send me a tip"](https://img.shields.io/badge/give%20me%20a%20-tip-FFD200)](https://paypal.me/emsicz) [!["see my github"](https://img.shields.io/badge/see%20my%20-github-1F2328)](https://github.com/masiton?tab=repositories) + +Lightweight, maintenance-free, stateless .NET REST API message broker for sending and receiving messages in deliver-once fashion. Trivial HTTP request/response consumption. Works with Sql or Npg. + +## Why + +Existing messaging solutions are not atractive (to me). They are complex, hard to set up, understand and maintain. Even if happy-day scenarios work well, or day-one setups seem easy, basic functionality is often hidden under obscure layer of arguments, parameters, settings and limitations put in place for reasons not quickly apparent. I needed a messaging system that is really simple, but sufficiently reliable and performant. I want to turn this on and forget about it forever. + +FOSS project for REST API over HTTP does not exist (or I didn't find it, [ChatGPT concurs](https://gitea.masita.net/mc/messengerapi/raw/branch/main/assets/why.jpg)). So here is my attempt. + +## How + +Messages and user credentials are kept in database. There are no usernames and passwords, only API keys. User can only message other users if corresponding route is set up between them. Manage users, routes and api keys directly in the DB. Clients authenticate and authorize their queries by using their API keys as Bearer tokens in Authorization headers permanently. + +## Containerization + +MessengerApi is built to be run as a container. See [image registry](https://gitea.masita.net/mc/-/packages/container/messengerapi/latest). + +## Auth & Security + +Senders and receivers send Bearer tokens with their HTTP request header: `Bearer ba53e34b-0163-40bc-9216-4ffa1fe3efb8`. *Users do not request their tokens, they are assigned tokens during registration and use them permanently.* This is a design choice done on purpose to put least possible amount of requirements on clients. A valid `route` must exist in DB between sender and receiver. Receivers can't ack someone else's message, and can't ack a message before it's delivered through `/receive` call. Access leak and password reset is handled through api key rotation when necessary. + +## Setup + +Mandatory tunables are super-simple: + +- `SQL_CONNECTIONSTRING` + - Must be provided. + - _Example: `"Persist Security Info=False;User ID=*****;Password=*****;Initial Catalog=AdventureWorks;Server=192.168.1.2"`_ + +- `CORS_ORIGINS` + - Must be provided, if you're consuming the API from browser. Comma separated. + - _Example: `www.mydomain.com,mydomain.com,anotherdomain.eu`_ + +### PostgreSQL + +To run MessengerApi against postgres, omit the `SQL_CONNECTIONSTRING` and use this: + +- `PERSISTENCE_TYPE: PostgreSql` + - This tells Messenger to use PostgreSql DB Context. +- `NPG_CONNECTIONSTRING` + - Must be provided. + - _Example: `"Host=localhost;Port=5432;Database=myDatabase;Username=postgres;Password=********`_ + +Additional tunables, with their sustainable default values: + +- `QUERY_RATE_PER_MINUTE: 100` + - Sets maximum allowed client query rate per minute for all endpoints. Anonymous users share same limit pool. + - If send rate is exceeded, client receives a `HTTP 429` with explanation. +- `DEFAULT_MESSAGE_LIFETIME_IN_MINUTES: 1` + - Message will wait for delivery for set amount of time. After the time passes, a call to `/receive` will not consider it for delivery anymore. + - Override this in message content by setting _optional_ `lifespanInSeconds` value inside the request. + - There will be no indication to the sender or to client that there was a missed message. Once it's gone, it's gone. +- `HOUSEKEEPING_ENABLED: true` + - Turns on housekeeping job that periodically removes stale, delivered and/or acknowledged messages. You can tune this further, see below. By default, it only removes messages that are 2 hours old, regardless of their delivery or acknowledgement state. +- `HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES: 120` + - Housekeeping job will delete any message older than set amount of time, regardless of it's current state. +- `HOUSEKEEPING_MESSAGE_STATE: NONE` + - Allowed values: `NONE`, `DELIVERED`, `ACKNOWLEDGED`. + - `NONE`: Housekeeping will not delete messages based on their state. + - `DELIVERED`: Housekeeping will delete messages that have been delivered. + - `ACKNOWLEDGED`: Housekeeping will delete messages that have been acknowledged. + - Works in addition to `HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES`, so messages can be removed earlier than after their age expires. + +### User management + +Manage users and credentials in DB manually. + +_Alternatively_, mount a config to the container's root dir with name of `users.conf` with structure that contains `username;id;apikey;comma_separated_allowed_recipients_if_any` per line: + + user1;90ddab90-0b73-4c6c-8dcb-2d8d1ec3c0b8;81ccf737-d424-4f83-929c-92d20491abfa;user2,user3,user4 + user2;8f5971c3-5e19-4b5c-88a7-e0ec4856ce44;f480568f-8884-47e5-a6d7-82480f1ffb3b;user1 + user3;f253a157-f336-4029-b90e-80a9f64b453b;46b882b7-4b96-4fa2-ba1b-4955a9500c36 + user4;5f20ec92-3168-4df5-b20d-5441d08b3f9a;51d11e51-efb2-43e9-beb8-52fb8e879bee;user2 + +Upon launch, Messenger will synchronize contents of the file with the database. Synchronization uses `id` as primary identifier to make it easy to rotate API keys and change names. Synchronization is done `users.conf => db` and treats the config file as single source of truth, meaning data present in the file but not in db will be added to db, and data not present in file, but present in db will be deleted from db. Editing the file and restarting the service will then update the data accordingly. + +## Integration + +Tunnel on port `80` to send traffic. Consume HTTP endpoints: + +### `POST /send` + +Sends message and returns it's ID. Minimal message body where `user2` sends a text message to `user1`, and since `user2` can only message to `user1` and nobody else, they don't even have to specify recipient, the system infers it automatically: + + http /send (post) header: Authorization: Bearer f480568f-8884-47e5-a6d7-82480f1ffb3b + { + "payload": "This is a message." + } + +Response + + http (json): + "5f33b4bd-dc2a-4ace-947a-1aadc6045995" + +Optionally, messages can be complex. Here is a message from `user1` to `user3`, both `payload` and `payloadType` are `nvarchar` fields and their content can be whatever: + + http /send (post) header: Authorization: Bearer 81ccf737-d424-4f83-929c-92d20491abfa + { + "payloadType": "STATUS", + "payload": "{\n \"system\": \"OK\",\n}", + "toUserId": "46b882b7-4b96-4fa2-ba1b-4955a9500c36", + "lifespanInSeconds": "3600" + } + +### `GET /receive` + +Receives all waiting messages. + + http /receive (get) header: Authorization: Bearer 81ccf737-d424-4f83-929c-92d20491abfa + +Response + + http (json): + "messages": [ + { + "id": "5f33b4bd-dc2a-4ace-947a-1aadc6045995", + "timestampUtc": "2025-07-04T16:32:12.4586339Z", + "payload": "{\n \"system\": \"OK\",\n}", + "payloadType": "STATUS", + "sender": "7bdea193-dce5-486e-ba7b-ec323d22bf90" + } + ] + +## Testing + +See postman collection to generate code for your desired language or test the API instance. The collection also works as test suite, just fill out the environment variables for URL and api keys of your users and it will run and test all endpoints, with expected outputs. \ No newline at end of file diff --git a/assets/why.jpg b/assets/why.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b7b7df91f9ac84fcafe59210cf8a294dd80ac2f1 GIT binary patch literal 171164 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 TNv&_TV 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 + + + net9.0 + enable + disable + + + diff --git a/code/MessengerApi.Configuration/Model/MessengerConfiguration.cs b/code/MessengerApi.Configuration/Model/MessengerConfiguration.cs new file mode 100644 index 0000000..2709c3c --- /dev/null +++ b/code/MessengerApi.Configuration/Model/MessengerConfiguration.cs @@ -0,0 +1,98 @@ +using MessengerApi.Configuration.Enums; +using MessengerApi.Configuration.Model.Persistence.Base; +using MessengerApi.Configuration.Parsers; +using MessengerApi.Configuration.Sources.Environment; +using Env = MessengerApi.Configuration.Constants.EnvironmentVariables; + +namespace MessengerApi.Configuration.Model +{ + public class MessengerConfiguration + { + /// + /// CORS origins. + /// + public string[] Origins { get; set; } + + /// + /// List of proxies that are trusted to provide forwarding headers. + /// + public string[] Proxies { get; set; } + + /// + /// Persistence layer configs (database). + /// + public PersistenceConfiguration PersistenceConfiguration { get; set; } + + /// + /// Limits rate of user calls to not DoS the service. + /// + public int RateLimitPerMinute { get; set; } + + /// + /// Message lifetime unless set differently in message body. + /// + public int DefaultMessageLifetimeInMinutes { get; set; } + + /// + /// If true, messages are periodically wiped to free up space. + /// + public bool HousekeepingEnabled { get; set; } + + /// + /// Messages older than value set will be deleted regardless of their delivery state. + /// + public int HousekeepingMessageAgeInMinutes { get; set; } + + /// + /// Determines level of log messages displayed. + /// + public LoggingVerbosity Verbosity { get; set; } + + /// + /// In addition to messages of certain state can also be deleted, increasing storage efficiency. + /// + public HousekeepingMessageStates HousekeepingMessageState { get; set; } + + public MessengerConfiguration() { } + + public MessengerConfiguration(string[] origins, PersistenceConfiguration persistenceConfiguration) + { + if(persistenceConfiguration == null) + { + throw new ArgumentNullException(nameof(persistenceConfiguration)); + } + + this.PersistenceConfiguration = persistenceConfiguration; + this.Origins = origins ?? []; + this.Proxies = []; + this.RateLimitPerMinute = 120; + this.DefaultMessageLifetimeInMinutes = 1; + this.HousekeepingEnabled = true; + this.HousekeepingMessageAgeInMinutes = 120; + this.HousekeepingMessageState = HousekeepingMessageStates.None; + this.Verbosity = LoggingVerbosity.Normal; + } + + public MessengerConfiguration(IEnvironmentConfigurationSource config) : this( + CorsParser.Parse(config.GetValue(Env.CORS_ORIGINS)), + EnvironmentPersistenceConfigurationParser.Parse(config)) + { + Populate(config, Env.PROXIES, x => this.Proxies = ProxiesParser.Parse(x)); + Populate(config, Env.QUERY_RATE_PER_MINUTE, x => this.RateLimitPerMinute = x); + Populate(config, Env.DEFAULT_MESSAGE_LIFETIME_IN_MINUTES, x => this.DefaultMessageLifetimeInMinutes = x); + Populate(config, Env.HOUSEKEEPING_ENABLED, x => this.HousekeepingEnabled = x); + Populate(config, Env.HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES, x => this.HousekeepingMessageAgeInMinutes = x); + Populate(config, Env.HOUSEKEEPING_MESSAGE_STATE, x => this.HousekeepingMessageState = HousekeepingMessageStateParser.Parse(x)); + Populate(config, Env.LOGGING_VERBOSITY, x => this.Verbosity = LoggingVerbosityParser.Parse(x)); + + void Populate(IEnvironmentConfigurationSource config, string key, Action set) + { + if (config.HasKey(key)) + { + var value = config.GetValue(key); + set(value); + } + } + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Configuration/Model/Persistence/Base/PersistenceConfiguration.cs b/code/MessengerApi.Configuration/Model/Persistence/Base/PersistenceConfiguration.cs new file mode 100644 index 0000000..361e153 --- /dev/null +++ b/code/MessengerApi.Configuration/Model/Persistence/Base/PersistenceConfiguration.cs @@ -0,0 +1,9 @@ +using MessengerApi.Configuration.Enums; + +namespace MessengerApi.Configuration.Model.Persistence.Base +{ + public abstract class PersistenceConfiguration + { + public abstract PersistenceTypes PersistenceType { get; } + } +} diff --git a/code/MessengerApi.Configuration/Model/Persistence/NpgPersistenceConfiguration.cs b/code/MessengerApi.Configuration/Model/Persistence/NpgPersistenceConfiguration.cs new file mode 100644 index 0000000..664ad53 --- /dev/null +++ b/code/MessengerApi.Configuration/Model/Persistence/NpgPersistenceConfiguration.cs @@ -0,0 +1,20 @@ +using MessengerApi.Configuration.Enums; +using MessengerApi.Configuration.Model.Persistence.Base; +using MessengerApi.Configuration.Sources.Environment; + +namespace MessengerApi.Configuration.Model.Persistence +{ + public class NpgPersistenceConfiguration : PersistenceConfiguration + { + public override PersistenceTypes PersistenceType => PersistenceTypes.PostgreSql; + + public string ConnectionString { get; } + + public NpgPersistenceConfiguration(string connectionString) + { + ConnectionString = connectionString; + } + + public NpgPersistenceConfiguration(IEnvironmentConfigurationSource config) : this(config.GetValue(Constants.EnvironmentVariables.NPG_CONNECTIONSTRING)) { } + } +} diff --git a/code/MessengerApi.Configuration/Model/Persistence/SqlPersistenceConfiguration.cs b/code/MessengerApi.Configuration/Model/Persistence/SqlPersistenceConfiguration.cs new file mode 100644 index 0000000..f7a3dfa --- /dev/null +++ b/code/MessengerApi.Configuration/Model/Persistence/SqlPersistenceConfiguration.cs @@ -0,0 +1,20 @@ +using MessengerApi.Configuration.Enums; +using MessengerApi.Configuration.Model.Persistence.Base; +using MessengerApi.Configuration.Sources.Environment; + +namespace MessengerApi.Configuration.Model.Persistence +{ + public class SqlPersistenceConfiguration : PersistenceConfiguration + { + public override PersistenceTypes PersistenceType => PersistenceTypes.Sql; + + public string ConnectionString { get; } + + public SqlPersistenceConfiguration(string connectionString) + { + ConnectionString = connectionString; + } + + public SqlPersistenceConfiguration(IEnvironmentConfigurationSource config) : this(config.GetValue(Constants.EnvironmentVariables.SQL_CONNECTIONSTRING)) { } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Configuration/Parsers/CorsParser.cs b/code/MessengerApi.Configuration/Parsers/CorsParser.cs new file mode 100644 index 0000000..8a97da4 --- /dev/null +++ b/code/MessengerApi.Configuration/Parsers/CorsParser.cs @@ -0,0 +1,11 @@ +namespace MessengerApi.Configuration.Parsers +{ + public static class CorsParser + { + public static string[] Parse(string value) + { + if (string.IsNullOrWhiteSpace(value)) return []; + return value.Trim().Split(",", StringSplitOptions.RemoveEmptyEntries); + } + } +} diff --git a/code/MessengerApi.Configuration/Parsers/EnvironmentPersistenceConfigurationParser.cs b/code/MessengerApi.Configuration/Parsers/EnvironmentPersistenceConfigurationParser.cs new file mode 100644 index 0000000..29d1332 --- /dev/null +++ b/code/MessengerApi.Configuration/Parsers/EnvironmentPersistenceConfigurationParser.cs @@ -0,0 +1,25 @@ +using MessengerApi.Configuration.Model.Persistence; +using MessengerApi.Configuration.Model.Persistence.Base; +using MessengerApi.Configuration.Sources.Environment; + +namespace MessengerApi.Configuration.Parsers +{ + public static class EnvironmentPersistenceConfigurationParser + { + public static PersistenceConfiguration Parse(IEnvironmentConfigurationSource config) + { + var type = PersistenceTypeParser.Parse(config.GetValue(Constants.EnvironmentVariables.PERSISTENCE_TYPE)); + + if(type == Enums.PersistenceTypes.Sql) + { + return new SqlPersistenceConfiguration(config); + } + else if(type == Enums.PersistenceTypes.PostgreSql) + { + return new NpgPersistenceConfiguration(config); + } + + throw new InvalidOperationException("Unrecognized persistence type."); + } + } +} diff --git a/code/MessengerApi.Configuration/Parsers/HousekeepingMessageStateParser.cs b/code/MessengerApi.Configuration/Parsers/HousekeepingMessageStateParser.cs new file mode 100644 index 0000000..34fd988 --- /dev/null +++ b/code/MessengerApi.Configuration/Parsers/HousekeepingMessageStateParser.cs @@ -0,0 +1,12 @@ +using MessengerApi.Configuration.Enums; + +namespace MessengerApi.Configuration.Parsers +{ + public static class HousekeepingMessageStateParser + { + public static HousekeepingMessageStates Parse(string input) + { + return (HousekeepingMessageStates)Enum.Parse(typeof(HousekeepingMessageStates), input.Trim(), true); + } + } +} diff --git a/code/MessengerApi.Configuration/Parsers/LoggingVerbosityParser.cs b/code/MessengerApi.Configuration/Parsers/LoggingVerbosityParser.cs new file mode 100644 index 0000000..92ce81a --- /dev/null +++ b/code/MessengerApi.Configuration/Parsers/LoggingVerbosityParser.cs @@ -0,0 +1,12 @@ +using MessengerApi.Configuration.Enums; + +namespace MessengerApi.Configuration.Parsers +{ + public static class LoggingVerbosityParser + { + public static LoggingVerbosity Parse(string value) + { + return (LoggingVerbosity)Enum.Parse(typeof(LoggingVerbosity), value.Trim(), true); + } + } +} diff --git a/code/MessengerApi.Configuration/Parsers/PersistenceTypeParser.cs b/code/MessengerApi.Configuration/Parsers/PersistenceTypeParser.cs new file mode 100644 index 0000000..37ea3e0 --- /dev/null +++ b/code/MessengerApi.Configuration/Parsers/PersistenceTypeParser.cs @@ -0,0 +1,12 @@ +using MessengerApi.Configuration.Enums; + +namespace MessengerApi.Configuration.Parsers +{ + public static class PersistenceTypeParser + { + public static PersistenceTypes Parse(string value) + { + return (PersistenceTypes)Enum.Parse(typeof(PersistenceTypes), value, true); + } + } +} diff --git a/code/MessengerApi.Configuration/Parsers/ProxiesParser.cs b/code/MessengerApi.Configuration/Parsers/ProxiesParser.cs new file mode 100644 index 0000000..8397847 --- /dev/null +++ b/code/MessengerApi.Configuration/Parsers/ProxiesParser.cs @@ -0,0 +1,11 @@ +namespace MessengerApi.Configuration.Parsers +{ + public static class ProxiesParser + { + public static string[] Parse(string value) + { + if (string.IsNullOrWhiteSpace(value)) return []; + return value.Trim().Split(",", StringSplitOptions.RemoveEmptyEntries); + } + } +} diff --git a/code/MessengerApi.Configuration/Sources/Environment/Constants.EnvironmentVariables.cs b/code/MessengerApi.Configuration/Sources/Environment/Constants.EnvironmentVariables.cs new file mode 100644 index 0000000..438dfc7 --- /dev/null +++ b/code/MessengerApi.Configuration/Sources/Environment/Constants.EnvironmentVariables.cs @@ -0,0 +1,20 @@ +namespace MessengerApi.Configuration +{ + public static partial class Constants + { + public static class EnvironmentVariables + { + public const string SQL_CONNECTIONSTRING = nameof(SQL_CONNECTIONSTRING); + public const string NPG_CONNECTIONSTRING = nameof(NPG_CONNECTIONSTRING); + public const string PERSISTENCE_TYPE = nameof(PERSISTENCE_TYPE); + public const string CORS_ORIGINS = nameof(CORS_ORIGINS); + public const string PROXIES = nameof(PROXIES); + public const string QUERY_RATE_PER_MINUTE = nameof(QUERY_RATE_PER_MINUTE); + public const string DEFAULT_MESSAGE_LIFETIME_IN_MINUTES = nameof(DEFAULT_MESSAGE_LIFETIME_IN_MINUTES); + public const string HOUSEKEEPING_ENABLED = nameof(HOUSEKEEPING_ENABLED); + public const string HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES = nameof(HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES); + public const string HOUSEKEEPING_MESSAGE_STATE = nameof(HOUSEKEEPING_MESSAGE_STATE); + public const string LOGGING_VERBOSITY = nameof(LOGGING_VERBOSITY); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Configuration/Sources/Environment/EnvironmentConfigurationSource.cs b/code/MessengerApi.Configuration/Sources/Environment/EnvironmentConfigurationSource.cs new file mode 100644 index 0000000..ca63b81 --- /dev/null +++ b/code/MessengerApi.Configuration/Sources/Environment/EnvironmentConfigurationSource.cs @@ -0,0 +1,15 @@ +namespace MessengerApi.Configuration.Sources.Environment +{ + public class EnvironmentConfigurationSource : IEnvironmentConfigurationSource + { + public bool HasKey(string key) + { + return !string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable(key)); + } + + public T GetValue(string key) + { + return (T)Convert.ChangeType(System.Environment.GetEnvironmentVariable(key), typeof(T)); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Configuration/Sources/Environment/IEnvironmentConfigurationSource.cs b/code/MessengerApi.Configuration/Sources/Environment/IEnvironmentConfigurationSource.cs new file mode 100644 index 0000000..16e5972 --- /dev/null +++ b/code/MessengerApi.Configuration/Sources/Environment/IEnvironmentConfigurationSource.cs @@ -0,0 +1,6 @@ +namespace MessengerApi.Configuration.Sources.Environment +{ + public interface IEnvironmentConfigurationSource : IConfigurationSource + { + } +} \ No newline at end of file diff --git a/code/MessengerApi.Configuration/Sources/IConfigurationSource.cs b/code/MessengerApi.Configuration/Sources/IConfigurationSource.cs new file mode 100644 index 0000000..9d090af --- /dev/null +++ b/code/MessengerApi.Configuration/Sources/IConfigurationSource.cs @@ -0,0 +1,9 @@ +namespace MessengerApi.Configuration.Sources +{ + public interface IConfigurationSource + { + bool HasKey(string key); + + T GetValue(string key); + } +} diff --git a/code/MessengerApi.Contracts.MessageParser/IMessageParser.cs b/code/MessengerApi.Contracts.MessageParser/IMessageParser.cs new file mode 100644 index 0000000..514a8ca --- /dev/null +++ b/code/MessengerApi.Contracts.MessageParser/IMessageParser.cs @@ -0,0 +1,22 @@ +namespace MessengerApi.Contracts.MessageParser +{ + /// + /// A tool that helps converting POCO request/response models into , and back. + /// + /// + /// If you implement this, it's gonna be a lot easier for you to translate + /// dumb request class into , then convert + /// back to request at server-side, and do the + /// same with the response all the way down to the client. + /// + public interface IMessageParser + { + OutboxMessage GetMessageFromRequest(TRequest request, int targetUserId); + + TRequest GetRequestFromMessage(InboxMessage message); + + OutboxMessage GetMessageFromResponse(TResponse response, string apiKey, int targetUserId, InboxMessage requestOrigin = null); + + TResponse GetResponseFromMessage(InboxMessage message); + } +} diff --git a/code/MessengerApi.Contracts.MessageParser/MessageParser.cs b/code/MessengerApi.Contracts.MessageParser/MessageParser.cs new file mode 100644 index 0000000..8bcdf3c --- /dev/null +++ b/code/MessengerApi.Contracts.MessageParser/MessageParser.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; + +namespace MessengerApi.Contracts.MessageParser +{ + public class MessageParser : IMessageParser + { + public OutboxMessage GetMessageFromRequest( + TRequest request, + int targetUserId) + { + var message = new OutboxMessage + { + + } + apikey, + targetUserId, + typeof(TRequest).Name, + JsonConvert.SerializeObject(request)); + + return message; + } + + public OutboxMessage GetMessageFromResponse( + TResponse response, + string apiKey, + int targetUserId, + InboxMessage requestOrigin = null) + { + var message = new OutboxMessage( + apiKey, + targetUserId, + requestOrigin.PayloadType, + JsonConvert.SerializeObject(response)); + + return message; + } + + public TRequest GetRequestFromMessage(InboxMessage message) + { + var request = JsonConvert.DeserializeObject(message.Payload); + return request; + } + + public TResponse GetResponseFromMessage(InboxMessage message) + { + var request = JsonConvert.DeserializeObject(message.Payload); + return request; + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Contracts.MessageParser/MessengerApi.Contracts.MessageParser.csproj b/code/MessengerApi.Contracts.MessageParser/MessengerApi.Contracts.MessageParser.csproj new file mode 100644 index 0000000..e2e019c --- /dev/null +++ b/code/MessengerApi.Contracts.MessageParser/MessengerApi.Contracts.MessageParser.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + enable + disable + $([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm")) + $([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm")) + True + ..\out\ + + + + + + + + diff --git a/code/MessengerApi.Contracts/Client/IMessengerClient.cs b/code/MessengerApi.Contracts/Client/IMessengerClient.cs new file mode 100644 index 0000000..a55c73e --- /dev/null +++ b/code/MessengerApi.Contracts/Client/IMessengerClient.cs @@ -0,0 +1,30 @@ +namespace MessengerApi.Contracts +{ + /// + /// Exists for mocking reason. This is implemented by . + /// + public interface IMessengerClient + { + /// + /// Receives pending messages from the messenger API. + /// + /// Credentials to the API. + IEnumerable GetMessages(); + + /// + /// Acknowledges message reception to the server. + /// + void AckMessage(InboxMessage message); + + /// + /// Sends a message. + /// + /// Credentials to the API. + void SendMessage(OutboxMessage outboxMessage); + + /// + /// Returns user ids for allowed message recipients. + /// + Contact[] GetYellowPages(); + } +} \ No newline at end of file diff --git a/code/MessengerApi.Contracts/Client/MessengerClient.cs b/code/MessengerApi.Contracts/Client/MessengerClient.cs new file mode 100644 index 0000000..5d0a12d --- /dev/null +++ b/code/MessengerApi.Contracts/Client/MessengerClient.cs @@ -0,0 +1,150 @@ +using portaloggy; +using System.Text; +using System.Text.Json.Nodes; + +namespace MessengerApi.Contracts +{ + public class MessengerClient : IMessengerClient + { + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + private DateTime _lastReceivedUtc; + private Credentials _credentials; + + public MessengerClient(Credentials credentials, HttpClient httpClient = null, ILogger logger = null) + { + _credentials = credentials; + _httpClient = httpClient ?? new HttpClient(); + _logger = logger ?? new ConsoleLogger(); + _lastReceivedUtc = DateTime.MinValue.ToUniversalTime(); + } + + public IEnumerable GetMessages() + { + var since = Uri.EscapeDataString(this._lastReceivedUtc.ToString("o")); + var url = $"{_credentials.ApiUrl}/receive?sinceUtc={since}"; + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _credentials.ApiKey); + + _logger.Debug($"Sending query to {url} with content {request.ToString()} to obtain messages."); + + var response = _httpClient.Send(request); + + if (!response.IsSuccessStatusCode) + { + _logger.Error(response.ReasonPhrase); + throw new HttpRequestException("Can't receive.", null, response.StatusCode); + } + + var responseContent = response.Content + .ReadAsStringAsync() + .GetAwaiter() + .GetResult(); + + if (!string.IsNullOrWhiteSpace(responseContent)) + { + _logger.Debug($"Received response of {responseContent}."); + } + + if (string.IsNullOrWhiteSpace(responseContent)) + { + return Enumerable.Empty().ToArray(); + } + + var json = JsonNode.Parse(responseContent); + var messages = new List(); + + foreach (var item in json["messages"].AsArray()) + { + if (item["id"].GetValue() == -1) + { + continue; + } + + messages.Add(new InboxMessage + { + Id = item["id"].GetValue(), + Payload = item["payload"].ToJsonString(), + PayloadId = item["payloadId"].ToJsonString(), + PayloadType = item["payloadType"].ToJsonString(), + Sender = item["sender"].GetValue(), + SenderTimestamp = item["senderTimestamp"].GetValue() + }); + } + + _lastReceivedUtc = DateTime.UtcNow.Subtract(TimeSpan.FromSeconds(10)); + _logger.Debug($"Received {messages.Count} messages and last check timestamp is set to {_lastReceivedUtc.ToString("s")}."); + + return messages.ToArray(); + } + + public void SendMessage(OutboxMessage outboxMessage) + { + var body = new JsonObject(); + + if(outboxMessage.ToUserId.HasValue) + { + body.Add("toUserId", JsonValue.Create(outboxMessage.ToUserId.Value)); + } + + if(outboxMessage.Payload != null) + { + body.Add("payload", JsonValue.Create(outboxMessage.Payload)); + } + + var content = new StringContent(body.ToString(), Encoding.UTF8, "application/json"); + + var url = $"{_credentials.ApiUrl}/send"; + var request = new HttpRequestMessage(HttpMethod.Post, url); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _credentials.ApiKey); + + _logger.Debug($"Sending query to {url} with content {body.ToString()} to obtain messages."); + + var response = _httpClient.Send(request); + + if (!response.IsSuccessStatusCode) + { + _logger.Error(response.ReasonPhrase); + throw new HttpRequestException("Can't send.", null, response.StatusCode); + } + } + + public Contact[] GetYellowPages() + { + var url = $"{_credentials.ApiUrl}/yellowpages"; + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _credentials.ApiKey); + + _logger.Debug($"Sending query to {url} with content {request.ToString()}."); + + var response = _httpClient.Send(request); + + if (!response.IsSuccessStatusCode) + { + _logger.Error(response.ReasonPhrase); + throw new HttpRequestException("Can't receive.", null, response.StatusCode); + } + + var responseContent = response.Content + .ReadAsStringAsync() + .GetAwaiter() + .GetResult(); + + var json = JsonNode.Parse(responseContent); + + var contacts = json["users"].AsArray().Select(x => new Contact + { + Id = x["id"].GetValue(), + Name = x["name"].GetValue() + }).ToArray(); + + return contacts; + } + + public void AckMessage(InboxMessage message) + { + throw new NotImplementedException(); + } + } +} diff --git a/code/MessengerApi.Contracts/Contact.cs b/code/MessengerApi.Contracts/Contact.cs new file mode 100644 index 0000000..1b232de --- /dev/null +++ b/code/MessengerApi.Contracts/Contact.cs @@ -0,0 +1,9 @@ +namespace MessengerApi.Contracts +{ + public class Contact + { + public Guid Id { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Contracts/Credentials.cs b/code/MessengerApi.Contracts/Credentials.cs new file mode 100644 index 0000000..483e43d --- /dev/null +++ b/code/MessengerApi.Contracts/Credentials.cs @@ -0,0 +1,15 @@ +namespace MessengerApi.Contracts +{ + public class Credentials + { + public string ApiKey { get; private set; } + + public string ApiUrl { get; private set; } + + public Credentials(string apiKey, string apiUrl) + { + ApiKey = apiKey; + ApiUrl = apiUrl; + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Contracts/Messages/InboxMessage.cs b/code/MessengerApi.Contracts/Messages/InboxMessage.cs new file mode 100644 index 0000000..c48d621 --- /dev/null +++ b/code/MessengerApi.Contracts/Messages/InboxMessage.cs @@ -0,0 +1,20 @@ +namespace MessengerApi.Contracts +{ + /// + /// Message when received is inbox. For server apps, this is request-type of message. For clients, this is a response-type of message. + /// + public class InboxMessage + { + public Guid Id { get; set; } + + public Guid Sender { get; set; } + + public DateTime? SenderTimestamp { get; set; } + + public string PayloadId { get; set; } + + public string PayloadType { get; set; } + + public string Payload { get; set; } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Contracts/Messages/OutboxMessage.cs b/code/MessengerApi.Contracts/Messages/OutboxMessage.cs new file mode 100644 index 0000000..b2ea665 --- /dev/null +++ b/code/MessengerApi.Contracts/Messages/OutboxMessage.cs @@ -0,0 +1,20 @@ +namespace MessengerApi.Contracts +{ + /// + /// Outbox type of message. A server-app will treat this as a response. A client app will treat this as a request. + /// + public class OutboxMessage + { + public Guid? ToUserId { get; set; } + + public string PayloadId { get; set; } + + public string PayloadType { get; set; } + + public string Payload { get; set; } + + public DateTime? PayloadTimestamp { get; set; } + + public int? PayloadLifespanInSeconds { get; set; } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Contracts/MessengerApi.Contracts.csproj b/code/MessengerApi.Contracts/MessengerApi.Contracts.csproj new file mode 100644 index 0000000..3b1f1a3 --- /dev/null +++ b/code/MessengerApi.Contracts/MessengerApi.Contracts.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + enable + disable + True + $(OutputPath) + $([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm")) + $([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm")) + ..\out\ + true + + + + + + \ No newline at end of file diff --git a/code/MessengerApi.Db.Contracts/Entities/IEntity.cs b/code/MessengerApi.Db.Contracts/Entities/IEntity.cs new file mode 100644 index 0000000..40cacb7 --- /dev/null +++ b/code/MessengerApi.Db.Contracts/Entities/IEntity.cs @@ -0,0 +1,11 @@ +namespace MessengerApi.Db.Contracts.Entities +{ + public interface IEntity + { + Guid Id { get; } + } + + public interface IEntity : IEntity where T : class, IEntity + { + } +} diff --git a/code/MessengerApi.Db.Contracts/Entities/Message.cs b/code/MessengerApi.Db.Contracts/Entities/Message.cs new file mode 100644 index 0000000..aa8b9a9 --- /dev/null +++ b/code/MessengerApi.Db.Contracts/Entities/Message.cs @@ -0,0 +1,25 @@ +using MessengerApi.Db.Contracts.Entities; + +namespace MessengerApi.Db.Entities +{ + public class Message : IEntity + { + public Guid Id { get; set; } + + public DateTime CreatedUtc { get; set; } + + public Guid FromId { get; set; } + + public Guid ToId { get; set; } + + public bool IsDelivered { get; set; } + + public bool IsAcknowledged { get; set; } + + public string PayloadType { get; set; } + + public string Payload { get; set; } + + public int? PayloadLifespanInSeconds { get; set; } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db.Contracts/Entities/User.cs b/code/MessengerApi.Db.Contracts/Entities/User.cs new file mode 100644 index 0000000..2f81a3f --- /dev/null +++ b/code/MessengerApi.Db.Contracts/Entities/User.cs @@ -0,0 +1,15 @@ +using MessengerApi.Db.Contracts.Entities; + +namespace MessengerApi.Db.Entities +{ + public class User : IEntity + { + public Guid Id { get; set; } + + public Guid ApiKey { get; set; } + + public string Name { get; set; } + + public bool IsEnabled { get; set; } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db.Contracts/Entities/UserRoute.cs b/code/MessengerApi.Db.Contracts/Entities/UserRoute.cs new file mode 100644 index 0000000..0e0f70b --- /dev/null +++ b/code/MessengerApi.Db.Contracts/Entities/UserRoute.cs @@ -0,0 +1,16 @@ +using MessengerApi.Db.Contracts.Entities; + +namespace MessengerApi.Db.Entities +{ + /// + /// Describes allowed message route (who can message whom). + /// + public class UserRoute : IEntity + { + public Guid Id { get; set; } + + public User From { get; set; } + + public User To { get; set; } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db.Contracts/MessengerApi.Db.Contracts.csproj b/code/MessengerApi.Db.Contracts/MessengerApi.Db.Contracts.csproj new file mode 100644 index 0000000..ef83a76 --- /dev/null +++ b/code/MessengerApi.Db.Contracts/MessengerApi.Db.Contracts.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + disable + + + diff --git a/code/MessengerApi.Db.Contracts/Repositories/IMessageRepository.cs b/code/MessengerApi.Db.Contracts/Repositories/IMessageRepository.cs new file mode 100644 index 0000000..a249593 --- /dev/null +++ b/code/MessengerApi.Db.Contracts/Repositories/IMessageRepository.cs @@ -0,0 +1,9 @@ +using MessengerApi.Db.Entities; + +namespace MessengerApi.Db.Contracts.Repositories +{ + public interface IMessageRepository : IRepository + { + IEnumerable GetPendingMessages(User user); + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db.Contracts/Repositories/IRepository.cs b/code/MessengerApi.Db.Contracts/Repositories/IRepository.cs new file mode 100644 index 0000000..4e7df01 --- /dev/null +++ b/code/MessengerApi.Db.Contracts/Repositories/IRepository.cs @@ -0,0 +1,15 @@ +using MessengerApi.Db.Contracts.Entities; + +namespace MessengerApi.Db.Contracts.Repositories +{ + public interface IRepository + { + } + + public interface IRepository : IRepository where T : class, IEntity + { + void Add(T entity); + + T GetById(Guid id); + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db.Contracts/Repositories/IUserRepository.cs b/code/MessengerApi.Db.Contracts/Repositories/IUserRepository.cs new file mode 100644 index 0000000..f36d0e2 --- /dev/null +++ b/code/MessengerApi.Db.Contracts/Repositories/IUserRepository.cs @@ -0,0 +1,9 @@ +using MessengerApi.Db.Entities; + +namespace MessengerApi.Db.Contracts.Repositories +{ + public interface IUserRepository : IRepository + { + User SingleByApiKeyAndEnabled(Guid id, bool enabled); + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db.Contracts/Repositories/IUserRouteRepository.cs b/code/MessengerApi.Db.Contracts/Repositories/IUserRouteRepository.cs new file mode 100644 index 0000000..8e55911 --- /dev/null +++ b/code/MessengerApi.Db.Contracts/Repositories/IUserRouteRepository.cs @@ -0,0 +1,17 @@ +using MessengerApi.Db.Entities; + +namespace MessengerApi.Db.Contracts.Repositories +{ + public interface IUserRouteRepository:IRepository + { + /// + /// Returns all routes for given user. + /// + IEnumerable GetAllByUser(User sender); + + /// + /// Returns routes where given user is sender. + /// + IEnumerable GetByFrom(User user); + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db.Npg.Migrator/DesignTimeDbContextFactory.cs b/code/MessengerApi.Db.Npg.Migrator/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..09e982f --- /dev/null +++ b/code/MessengerApi.Db.Npg.Migrator/DesignTimeDbContextFactory.cs @@ -0,0 +1,13 @@ +using MessengerApi.Db.Npg; +using Microsoft.EntityFrameworkCore.Design; + +namespace MessengerApi.Db.Sql.Migrator +{ + public partial class DesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + public MessengerNpgDbContext CreateDbContext(string[] args) + { + return new MessengerNpgDbContext(this.ConnectionString); + } + } +} diff --git a/code/MessengerApi.Db.Npg.Migrator/MessengerApi.Db.Npg.Migrator.csproj b/code/MessengerApi.Db.Npg.Migrator/MessengerApi.Db.Npg.Migrator.csproj new file mode 100644 index 0000000..cd92fc0 --- /dev/null +++ b/code/MessengerApi.Db.Npg.Migrator/MessengerApi.Db.Npg.Migrator.csproj @@ -0,0 +1,21 @@ + + + + Exe + net9.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/code/MessengerApi.Db.Npg.Migrator/Program.cs b/code/MessengerApi.Db.Npg.Migrator/Program.cs new file mode 100644 index 0000000..6f016db --- /dev/null +++ b/code/MessengerApi.Db.Npg.Migrator/Program.cs @@ -0,0 +1,16 @@ +namespace MessengerApi.Db.Npg.Migrator +{ + internal class Program + { + static void Main(string[] args) + { + // You can use empty string to build the context when adding a migration - adding a migration does not hit the DB. + + // DesignTimeDbFactory.ConnectionString.cs is not versioned on purposed. Add missing property file for this partial class and then run this command: + // Add-Migration YourMigration -Project MessengerApi.Db.Npg -StartupProject MessengerApi.Db.Npg.Migrator -Verbose -Context MessengerNpgDbContext + + // To update the database, make sure your connection string is correct and run this command: + // Update-Database -Project MessengerApi.Db.Npg -StartupProject MessengerApi.Db.Npg.Migrator -Verbose -Context MessengerNpgDbContext + } + } +} diff --git a/code/MessengerApi.Db.Npg/MessengerApi.Db.Npg.csproj b/code/MessengerApi.Db.Npg/MessengerApi.Db.Npg.csproj new file mode 100644 index 0000000..e6f0e7b --- /dev/null +++ b/code/MessengerApi.Db.Npg/MessengerApi.Db.Npg.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + diff --git a/code/MessengerApi.Db.Npg/MessengerNpgDbContext.cs b/code/MessengerApi.Db.Npg/MessengerNpgDbContext.cs new file mode 100644 index 0000000..aa95d7a --- /dev/null +++ b/code/MessengerApi.Db.Npg/MessengerNpgDbContext.cs @@ -0,0 +1,31 @@ +using MessengerApi.Db.Entities; +using Microsoft.EntityFrameworkCore; + +namespace MessengerApi.Db.Npg +{ + public class MessengerNpgDbContext : MessengerDbContext + { + private readonly string connectionString; + + public MessengerNpgDbContext(string connectionString) + { + this.connectionString = connectionString; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseNpgsql(this.connectionString); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // https://stackoverflow.com/questions/26464357/what-is-the-purpose-of-non-unique-indexes-in-a-database + // https://stackoverflow.com/questions/40767980/generate-a-composite-unique-constraint-index-in-ef-core + // https://www.geeksforgeeks.org/difference-between-clustered-and-non-clustered-index/ + modelBuilder.Entity().HasIndex(e => new { e.ToId, e.IsDelivered }).IsUnique(false); + } + } +} diff --git a/code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.Designer.cs b/code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.Designer.cs new file mode 100644 index 0000000..b8c2870 --- /dev/null +++ b/code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.Designer.cs @@ -0,0 +1,123 @@ +// +using System; +using MessengerApi.Db.Npg; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MessengerApi.Db.Npg.Migrations +{ + [DbContext(typeof(MessengerNpgDbContext))] + [Migration("20250704170425_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MessengerApi.Db.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("FromId") + .HasColumnType("uuid"); + + b.Property("IsAcknowledged") + .HasColumnType("boolean"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("PayloadLifespanInSeconds") + .HasColumnType("integer"); + + b.Property("PayloadType") + .HasColumnType("text"); + + b.Property("ToId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ToId", "IsDelivered"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FromId") + .HasColumnType("uuid"); + + b.Property("ToId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("FromId"); + + b.HasIndex("ToId"); + + b.ToTable("UserRoutes"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b => + { + b.HasOne("MessengerApi.Db.Entities.User", "From") + .WithMany() + .HasForeignKey("FromId"); + + b.HasOne("MessengerApi.Db.Entities.User", "To") + .WithMany() + .HasForeignKey("ToId"); + + b.Navigation("From"); + + b.Navigation("To"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.cs b/code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.cs new file mode 100644 index 0000000..521e562 --- /dev/null +++ b/code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.cs @@ -0,0 +1,99 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MessengerApi.Db.Npg.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Messages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CreatedUtc = table.Column(type: "timestamp with time zone", nullable: false), + FromId = table.Column(type: "uuid", nullable: false), + ToId = table.Column(type: "uuid", nullable: false), + IsDelivered = table.Column(type: "boolean", nullable: false), + IsAcknowledged = table.Column(type: "boolean", nullable: false), + PayloadType = table.Column(type: "text", nullable: true), + Payload = table.Column(type: "text", nullable: true), + PayloadLifespanInSeconds = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Messages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ApiKey = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: true), + IsEnabled = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UserRoutes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FromId = table.Column(type: "uuid", nullable: true), + ToId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRoutes", x => x.Id); + table.ForeignKey( + name: "FK_UserRoutes_Users_FromId", + column: x => x.FromId, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_UserRoutes_Users_ToId", + column: x => x.ToId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Messages_ToId_IsDelivered", + table: "Messages", + columns: new[] { "ToId", "IsDelivered" }); + + migrationBuilder.CreateIndex( + name: "IX_UserRoutes_FromId", + table: "UserRoutes", + column: "FromId"); + + migrationBuilder.CreateIndex( + name: "IX_UserRoutes_ToId", + table: "UserRoutes", + column: "ToId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Messages"); + + migrationBuilder.DropTable( + name: "UserRoutes"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/code/MessengerApi.Db.Npg/Migrations/MessengerNpgDbContextModelSnapshot.cs b/code/MessengerApi.Db.Npg/Migrations/MessengerNpgDbContextModelSnapshot.cs new file mode 100644 index 0000000..66307cb --- /dev/null +++ b/code/MessengerApi.Db.Npg/Migrations/MessengerNpgDbContextModelSnapshot.cs @@ -0,0 +1,120 @@ +// +using System; +using MessengerApi.Db.Npg; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MessengerApi.Db.Npg.Migrations +{ + [DbContext(typeof(MessengerNpgDbContext))] + partial class MessengerNpgDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MessengerApi.Db.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("FromId") + .HasColumnType("uuid"); + + b.Property("IsAcknowledged") + .HasColumnType("boolean"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("PayloadLifespanInSeconds") + .HasColumnType("integer"); + + b.Property("PayloadType") + .HasColumnType("text"); + + b.Property("ToId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ToId", "IsDelivered"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasColumnType("uuid"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FromId") + .HasColumnType("uuid"); + + b.Property("ToId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("FromId"); + + b.HasIndex("ToId"); + + b.ToTable("UserRoutes"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b => + { + b.HasOne("MessengerApi.Db.Entities.User", "From") + .WithMany() + .HasForeignKey("FromId"); + + b.HasOne("MessengerApi.Db.Entities.User", "To") + .WithMany() + .HasForeignKey("ToId"); + + b.Navigation("From"); + + b.Navigation("To"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/MessengerApi.Db.Sql.Migrator/DesignTimeDbContextFactory.cs b/code/MessengerApi.Db.Sql.Migrator/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..b0edeb4 --- /dev/null +++ b/code/MessengerApi.Db.Sql.Migrator/DesignTimeDbContextFactory.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore.Design; + +namespace MessengerApi.Db.Sql.Migrator +{ + public partial class DesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + public MessengerSqlDbContext CreateDbContext(string[] args) + { + return new MessengerSqlDbContext(this.ConnectionString); + } + } +} diff --git a/code/MessengerApi.Db.Sql.Migrator/MessengerApi.Db.Sql.Migrator.csproj b/code/MessengerApi.Db.Sql.Migrator/MessengerApi.Db.Sql.Migrator.csproj new file mode 100644 index 0000000..dc203c9 --- /dev/null +++ b/code/MessengerApi.Db.Sql.Migrator/MessengerApi.Db.Sql.Migrator.csproj @@ -0,0 +1,22 @@ + + + + Exe + net9.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/code/MessengerApi.Db.Sql.Migrator/Program.cs b/code/MessengerApi.Db.Sql.Migrator/Program.cs new file mode 100644 index 0000000..4882f97 --- /dev/null +++ b/code/MessengerApi.Db.Sql.Migrator/Program.cs @@ -0,0 +1,16 @@ +namespace MessengerApi.Db.Sql.Migrator +{ + internal class Program + { + static void Main(string[] args) + { + // You can use empty string to build the context when adding a migration - adding a migration does not hit the DB. + + // DesignTimeDbFactory.ConnectionString.cs is not versioned on purposed. Add missing property file for this partial class and then run this command: + // Add-Migration YourMigration -Project MessengerApi.Db.Sql -StartupProject MessengerApi.Db.Sql.Migrator -Verbose -Context MessengerSqlDbContext + + // To update the database, make sure your connection string is correct and run this command: + // Update-Database -Project MessengerApi.Db.Sql -StartupProject MessengerApi.Db.Sql.Migrator -Verbose -Context MessengerSqlDbContext + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db.Sql/MessengerApi.Db.Sql.csproj b/code/MessengerApi.Db.Sql/MessengerApi.Db.Sql.csproj new file mode 100644 index 0000000..0372cd2 --- /dev/null +++ b/code/MessengerApi.Db.Sql/MessengerApi.Db.Sql.csproj @@ -0,0 +1,21 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/code/MessengerApi.Db.Sql/MessengerSqlDbContext.cs b/code/MessengerApi.Db.Sql/MessengerSqlDbContext.cs new file mode 100644 index 0000000..6c69b6d --- /dev/null +++ b/code/MessengerApi.Db.Sql/MessengerSqlDbContext.cs @@ -0,0 +1,31 @@ +using MessengerApi.Db.Entities; +using Microsoft.EntityFrameworkCore; + +namespace MessengerApi.Db.Sql +{ + public class MessengerSqlDbContext : MessengerDbContext + { + private readonly string connectionString; + + public MessengerSqlDbContext(string connectionString) + { + this.connectionString = connectionString; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlServer(this.connectionString); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // https://stackoverflow.com/questions/26464357/what-is-the-purpose-of-non-unique-indexes-in-a-database + // https://stackoverflow.com/questions/40767980/generate-a-composite-unique-constraint-index-in-ef-core + // https://www.geeksforgeeks.org/difference-between-clustered-and-non-clustered-index/ + modelBuilder.Entity().HasIndex(e => new { e.ToId, e.IsDelivered }).IsUnique(false).IsClustered(false); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.Designer.cs b/code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.Designer.cs new file mode 100644 index 0000000..4ae31a3 --- /dev/null +++ b/code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.Designer.cs @@ -0,0 +1,125 @@ +// +using System; +using MessengerApi.Db.Sql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MessengerApi.Db.Sql.Migrations +{ + [DbContext(typeof(MessengerSqlDbContext))] + [Migration("20250704165018_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("MessengerApi.Db.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("FromId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsAcknowledged") + .HasColumnType("bit"); + + b.Property("IsDelivered") + .HasColumnType("bit"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("PayloadLifespanInSeconds") + .HasColumnType("int"); + + b.Property("PayloadType") + .HasColumnType("nvarchar(max)"); + + b.Property("ToId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ToId", "IsDelivered"); + + SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("ToId", "IsDelivered"), false); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApiKey") + .HasColumnType("uniqueidentifier"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FromId") + .HasColumnType("uniqueidentifier"); + + b.Property("ToId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("FromId"); + + b.HasIndex("ToId"); + + b.ToTable("UserRoutes"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b => + { + b.HasOne("MessengerApi.Db.Entities.User", "From") + .WithMany() + .HasForeignKey("FromId"); + + b.HasOne("MessengerApi.Db.Entities.User", "To") + .WithMany() + .HasForeignKey("ToId"); + + b.Navigation("From"); + + b.Navigation("To"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.cs b/code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.cs new file mode 100644 index 0000000..e84ea41 --- /dev/null +++ b/code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MessengerApi.Db.Sql.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Messages", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + CreatedUtc = table.Column(type: "datetime2", nullable: false), + FromId = table.Column(type: "uniqueidentifier", nullable: false), + ToId = table.Column(type: "uniqueidentifier", nullable: false), + IsDelivered = table.Column(type: "bit", nullable: false), + IsAcknowledged = table.Column(type: "bit", nullable: false), + PayloadType = table.Column(type: "nvarchar(max)", nullable: true), + Payload = table.Column(type: "nvarchar(max)", nullable: true), + PayloadLifespanInSeconds = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Messages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ApiKey = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: true), + IsEnabled = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UserRoutes", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + FromId = table.Column(type: "uniqueidentifier", nullable: true), + ToId = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRoutes", x => x.Id); + table.ForeignKey( + name: "FK_UserRoutes_Users_FromId", + column: x => x.FromId, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_UserRoutes_Users_ToId", + column: x => x.ToId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Messages_ToId_IsDelivered", + table: "Messages", + columns: new[] { "ToId", "IsDelivered" }) + .Annotation("SqlServer:Clustered", false); + + migrationBuilder.CreateIndex( + name: "IX_UserRoutes_FromId", + table: "UserRoutes", + column: "FromId"); + + migrationBuilder.CreateIndex( + name: "IX_UserRoutes_ToId", + table: "UserRoutes", + column: "ToId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Messages"); + + migrationBuilder.DropTable( + name: "UserRoutes"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/code/MessengerApi.Db.Sql/Migrations/MessengerSqlDbContextModelSnapshot.cs b/code/MessengerApi.Db.Sql/Migrations/MessengerSqlDbContextModelSnapshot.cs new file mode 100644 index 0000000..6faf21c --- /dev/null +++ b/code/MessengerApi.Db.Sql/Migrations/MessengerSqlDbContextModelSnapshot.cs @@ -0,0 +1,122 @@ +// +using System; +using MessengerApi.Db.Sql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MessengerApi.Db.Sql.Migrations +{ + [DbContext(typeof(MessengerSqlDbContext))] + partial class MessengerSqlDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("MessengerApi.Db.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("FromId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsAcknowledged") + .HasColumnType("bit"); + + b.Property("IsDelivered") + .HasColumnType("bit"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("PayloadLifespanInSeconds") + .HasColumnType("int"); + + b.Property("PayloadType") + .HasColumnType("nvarchar(max)"); + + b.Property("ToId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ToId", "IsDelivered"); + + SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("ToId", "IsDelivered"), false); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApiKey") + .HasColumnType("uniqueidentifier"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FromId") + .HasColumnType("uniqueidentifier"); + + b.Property("ToId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("FromId"); + + b.HasIndex("ToId"); + + b.ToTable("UserRoutes"); + }); + + modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b => + { + b.HasOne("MessengerApi.Db.Entities.User", "From") + .WithMany() + .HasForeignKey("FromId"); + + b.HasOne("MessengerApi.Db.Entities.User", "To") + .WithMany() + .HasForeignKey("ToId"); + + b.Navigation("From"); + + b.Navigation("To"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/MessengerApi.Db/Converters/DateTimeAsUtcValueConverter.cs b/code/MessengerApi.Db/Converters/DateTimeAsUtcValueConverter.cs new file mode 100644 index 0000000..3e62e6a --- /dev/null +++ b/code/MessengerApi.Db/Converters/DateTimeAsUtcValueConverter.cs @@ -0,0 +1,8 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace MessengerApi.Db.Converters +{ + public sealed class DateTimeAsUtcValueConverter() + : ValueConverter( + v => v, v => new DateTime(v.Ticks, DateTimeKind.Utc)); +} diff --git a/code/MessengerApi.Db/MessengerApi.Db.csproj b/code/MessengerApi.Db/MessengerApi.Db.csproj new file mode 100644 index 0000000..b052c4c --- /dev/null +++ b/code/MessengerApi.Db/MessengerApi.Db.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + disable + ..\out\ + true + + + + + + + + + + + diff --git a/code/MessengerApi.Db/MessengerDbContext.cs b/code/MessengerApi.Db/MessengerDbContext.cs new file mode 100644 index 0000000..9c598b1 --- /dev/null +++ b/code/MessengerApi.Db/MessengerDbContext.cs @@ -0,0 +1,26 @@ +using MessengerApi.Db.Converters; +using MessengerApi.Db.Entities; +using Microsoft.EntityFrameworkCore; + +namespace MessengerApi.Db +{ + public abstract class MessengerDbContext : DbContext + { + public DbSet Users { get; set; } + + public DbSet Messages { get; set; } + + public DbSet UserRoutes { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().HasKey(e => e.Id); + modelBuilder.Entity().HasKey(e => e.Id); + modelBuilder.Entity().Property(e => e.CreatedUtc).HasConversion(); + modelBuilder.Entity().Property(e => e.PayloadLifespanInSeconds).IsRequired(); + modelBuilder.Entity().HasKey(e => e.Id); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db/Repositories/MessageRepository.cs b/code/MessengerApi.Db/Repositories/MessageRepository.cs new file mode 100644 index 0000000..bd305a1 --- /dev/null +++ b/code/MessengerApi.Db/Repositories/MessageRepository.cs @@ -0,0 +1,23 @@ +using MessengerApi.Db.Contracts.Repositories; +using MessengerApi.Db.Entities; +using Microsoft.EntityFrameworkCore; + +namespace MessengerApi.Db.Repositories +{ + public class MessageRepository : Repository, IMessageRepository + { + public MessageRepository(DbSet db) : base(db) + { + } + + public IEnumerable GetPendingMessages(User user) + { + var timestamp = DateTime.UtcNow; + + return this.db + .Where(x => x.ToId == user.Id && x.IsDelivered == false) + .Where(x => x.PayloadLifespanInSeconds == null || x.CreatedUtc.AddSeconds(x.PayloadLifespanInSeconds.Value) >= timestamp) + .OrderBy(x => x.CreatedUtc); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db/Repositories/Repository.cs b/code/MessengerApi.Db/Repositories/Repository.cs new file mode 100644 index 0000000..5fa6b8d --- /dev/null +++ b/code/MessengerApi.Db/Repositories/Repository.cs @@ -0,0 +1,26 @@ +using MessengerApi.Db.Contracts.Entities; +using MessengerApi.Db.Contracts.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace MessengerApi.Db.Repositories +{ + public abstract class Repository : IRepository where T : class, IEntity + { + protected readonly DbSet db; + + public Repository(DbSet db) + { + this.db = db; + } + + public void Add(T entity) + { + this.db.Add(entity); + } + + public T GetById(Guid id) + { + return this.db.Single(x => x.Id == id); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db/Repositories/UserRepository.cs b/code/MessengerApi.Db/Repositories/UserRepository.cs new file mode 100644 index 0000000..23d299e --- /dev/null +++ b/code/MessengerApi.Db/Repositories/UserRepository.cs @@ -0,0 +1,18 @@ +using MessengerApi.Db.Contracts.Repositories; +using MessengerApi.Db.Entities; +using Microsoft.EntityFrameworkCore; + +namespace MessengerApi.Db.Repositories +{ + public class UserRepository : Repository, IUserRepository + { + public UserRepository(DbSet db) : base(db) + { + } + + public User SingleByApiKeyAndEnabled(Guid id, bool enabled) + { + return this.db.Single(x => x.ApiKey == id && x.IsEnabled == enabled); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.Db/Repositories/UserRouteRepository.cs b/code/MessengerApi.Db/Repositories/UserRouteRepository.cs new file mode 100644 index 0000000..9403ce6 --- /dev/null +++ b/code/MessengerApi.Db/Repositories/UserRouteRepository.cs @@ -0,0 +1,23 @@ +using MessengerApi.Db.Contracts.Repositories; +using MessengerApi.Db.Entities; +using Microsoft.EntityFrameworkCore; + +namespace MessengerApi.Db.Repositories +{ + public class UserRouteRepository : Repository, IUserRouteRepository + { + public UserRouteRepository(DbSet db) : base(db) + { + } + + public IEnumerable GetAllByUser(User sender) + { + return this.db.Include(x => x.From).Include(x => x.To).Where(x => x.From.Id == sender.Id || x.To.Id == sender.Id); + } + + public IEnumerable GetByFrom(User user) + { + return this.db.Include(x => x.From).Include(x => x.To).Where(x => x.From.Id == user.Id); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi.sln b/code/MessengerApi.sln new file mode 100644 index 0000000..85a6f9a --- /dev/null +++ b/code/MessengerApi.sln @@ -0,0 +1,156 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35312.102 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Contracts", "MessengerApi.Contracts\MessengerApi.Contracts.csproj", "{833ED77F-A4E9-4FB3-BB84-4E55898B726A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.SubscriptionClient", "MessengerApi.SubscriptionClient\MessengerApi.SubscriptionClient.csproj", "{127D24B0-47F3-40E9-9136-899AFF206F19}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.QueryClient", "MessengerApi.QueryClient\MessengerApi.QueryClient.csproj", "{6441673B-2621-4E2C-A9A0-971E83C3F80A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{60B75400-A315-4B57-AFCF-5B4094785A62}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Examples.PhonebookClient", "MessengerApi.Examples.PhonebookClient\MessengerApi.Examples.PhonebookClient.csproj", "{D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Examples.SnapshotSubscriptionClient", "MessengerApi.Examples.SnapshotSubscriptionClient\MessengerApi.Examples.SnapshotSubscriptionClient.csproj", "{A57429EB-3929-4E8B-B427-9B77D14CC486}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Examples.Constants", "MessengerApi.Examples.Constants\MessengerApi.Examples.Constants.csproj", "{7EC1857B-5BFD-46F6-809D-CE617CFD9A8C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Examples.QueryClient", "MessengerApi.Examples.QueryClient\MessengerApi.Examples.QueryClient.csproj", "{09DEF168-FD5C-47C3-81DF-077BE6219089}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Db", "MessengerApi.Db\MessengerApi.Db.csproj", "{64B33C4B-4B04-4F48-8620-4CA2AB641934}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi", "MessengerApi\MessengerApi.csproj", "{BA717183-65C4-4568-8ACD-DEDBD2B77322}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Db", "Db", "{F318E6F5-0343-491B-9264-CFFB4CCF1241}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{6FCD97D3-1EC8-4BB0-8BE1-245B9E51565A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Tests.LongTermSendingClient", "MessengerApi.Tests.LongTermSendingClient\MessengerApi.Tests.LongTermSendingClient.csproj", "{BAFCEB19-4FFC-44DF-8240-93172191080F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Tests.LongTermReceivingClient", "MessengerApi.Tests.LongTermReceivingClient\MessengerApi.Tests.LongTermReceivingClient.csproj", "{FE628370-BD9E-4745-8C5B-EDAA44BBA2BB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" + ProjectSection(SolutionItems) = preProject + ..\Directory.Packages.props = ..\Directory.Packages.props + ..\docker-compose.yml = ..\docker-compose.yml + ..\Dockerfile = ..\Dockerfile + ..\NuGet.config = ..\NuGet.config + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + ..\.gitea\workflows\build.yml = ..\.gitea\workflows\build.yml + ..\.gitea\workflows\docker-build-and-push.yml = ..\.gitea\workflows\docker-build-and-push.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Configuration", "MessengerApi.Configuration\MessengerApi.Configuration.csproj", "{4588FB85-FD64-4B7F-B37A-6F2ADD403E80}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Db.Sql", "MessengerApi.Db.Sql\MessengerApi.Db.Sql.csproj", "{22755F3D-C55D-436C-9C9F-C564001B976B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Db.Npg", "MessengerApi.Db.Npg\MessengerApi.Db.Npg.csproj", "{8199D547-23AC-4B10-9BD1-2996A6C35B1D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Db.Contracts", "MessengerApi.Db.Contracts\MessengerApi.Db.Contracts.csproj", "{062ADC2E-EF77-4319-9269-D60D39E31C0E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Db.Sql.Migrator", "MessengerApi.Db.Sql.Migrator\MessengerApi.Db.Sql.Migrator.csproj", "{65C395EC-81E9-4919-9721-72CAA3E4780D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Db.Npg.Migrator", "MessengerApi.Db.Npg.Migrator\MessengerApi.Db.Npg.Migrator.csproj", "{DF751DD1-9869-4916-B946-A8513A7CE706}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {833ED77F-A4E9-4FB3-BB84-4E55898B726A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {833ED77F-A4E9-4FB3-BB84-4E55898B726A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {833ED77F-A4E9-4FB3-BB84-4E55898B726A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {833ED77F-A4E9-4FB3-BB84-4E55898B726A}.Release|Any CPU.Build.0 = Release|Any CPU + {127D24B0-47F3-40E9-9136-899AFF206F19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {127D24B0-47F3-40E9-9136-899AFF206F19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {127D24B0-47F3-40E9-9136-899AFF206F19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {127D24B0-47F3-40E9-9136-899AFF206F19}.Release|Any CPU.Build.0 = Release|Any CPU + {6441673B-2621-4E2C-A9A0-971E83C3F80A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6441673B-2621-4E2C-A9A0-971E83C3F80A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6441673B-2621-4E2C-A9A0-971E83C3F80A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6441673B-2621-4E2C-A9A0-971E83C3F80A}.Release|Any CPU.Build.0 = Release|Any CPU + {D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF}.Release|Any CPU.Build.0 = Release|Any CPU + {A57429EB-3929-4E8B-B427-9B77D14CC486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A57429EB-3929-4E8B-B427-9B77D14CC486}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A57429EB-3929-4E8B-B427-9B77D14CC486}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A57429EB-3929-4E8B-B427-9B77D14CC486}.Release|Any CPU.Build.0 = Release|Any CPU + {7EC1857B-5BFD-46F6-809D-CE617CFD9A8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EC1857B-5BFD-46F6-809D-CE617CFD9A8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EC1857B-5BFD-46F6-809D-CE617CFD9A8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EC1857B-5BFD-46F6-809D-CE617CFD9A8C}.Release|Any CPU.Build.0 = Release|Any CPU + {09DEF168-FD5C-47C3-81DF-077BE6219089}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09DEF168-FD5C-47C3-81DF-077BE6219089}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09DEF168-FD5C-47C3-81DF-077BE6219089}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09DEF168-FD5C-47C3-81DF-077BE6219089}.Release|Any CPU.Build.0 = Release|Any CPU + {64B33C4B-4B04-4F48-8620-4CA2AB641934}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64B33C4B-4B04-4F48-8620-4CA2AB641934}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64B33C4B-4B04-4F48-8620-4CA2AB641934}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64B33C4B-4B04-4F48-8620-4CA2AB641934}.Release|Any CPU.Build.0 = Release|Any CPU + {BA717183-65C4-4568-8ACD-DEDBD2B77322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA717183-65C4-4568-8ACD-DEDBD2B77322}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA717183-65C4-4568-8ACD-DEDBD2B77322}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA717183-65C4-4568-8ACD-DEDBD2B77322}.Release|Any CPU.Build.0 = Release|Any CPU + {BAFCEB19-4FFC-44DF-8240-93172191080F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAFCEB19-4FFC-44DF-8240-93172191080F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAFCEB19-4FFC-44DF-8240-93172191080F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAFCEB19-4FFC-44DF-8240-93172191080F}.Release|Any CPU.Build.0 = Release|Any CPU + {FE628370-BD9E-4745-8C5B-EDAA44BBA2BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE628370-BD9E-4745-8C5B-EDAA44BBA2BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE628370-BD9E-4745-8C5B-EDAA44BBA2BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE628370-BD9E-4745-8C5B-EDAA44BBA2BB}.Release|Any CPU.Build.0 = Release|Any CPU + {4588FB85-FD64-4B7F-B37A-6F2ADD403E80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4588FB85-FD64-4B7F-B37A-6F2ADD403E80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4588FB85-FD64-4B7F-B37A-6F2ADD403E80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4588FB85-FD64-4B7F-B37A-6F2ADD403E80}.Release|Any CPU.Build.0 = Release|Any CPU + {22755F3D-C55D-436C-9C9F-C564001B976B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22755F3D-C55D-436C-9C9F-C564001B976B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22755F3D-C55D-436C-9C9F-C564001B976B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22755F3D-C55D-436C-9C9F-C564001B976B}.Release|Any CPU.Build.0 = Release|Any CPU + {8199D547-23AC-4B10-9BD1-2996A6C35B1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8199D547-23AC-4B10-9BD1-2996A6C35B1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8199D547-23AC-4B10-9BD1-2996A6C35B1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8199D547-23AC-4B10-9BD1-2996A6C35B1D}.Release|Any CPU.Build.0 = Release|Any CPU + {062ADC2E-EF77-4319-9269-D60D39E31C0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {062ADC2E-EF77-4319-9269-D60D39E31C0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {062ADC2E-EF77-4319-9269-D60D39E31C0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {062ADC2E-EF77-4319-9269-D60D39E31C0E}.Release|Any CPU.Build.0 = Release|Any CPU + {65C395EC-81E9-4919-9721-72CAA3E4780D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65C395EC-81E9-4919-9721-72CAA3E4780D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65C395EC-81E9-4919-9721-72CAA3E4780D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65C395EC-81E9-4919-9721-72CAA3E4780D}.Release|Any CPU.Build.0 = Release|Any CPU + {DF751DD1-9869-4916-B946-A8513A7CE706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF751DD1-9869-4916-B946-A8513A7CE706}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF751DD1-9869-4916-B946-A8513A7CE706}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF751DD1-9869-4916-B946-A8513A7CE706}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF} = {60B75400-A315-4B57-AFCF-5B4094785A62} + {A57429EB-3929-4E8B-B427-9B77D14CC486} = {60B75400-A315-4B57-AFCF-5B4094785A62} + {7EC1857B-5BFD-46F6-809D-CE617CFD9A8C} = {60B75400-A315-4B57-AFCF-5B4094785A62} + {09DEF168-FD5C-47C3-81DF-077BE6219089} = {60B75400-A315-4B57-AFCF-5B4094785A62} + {64B33C4B-4B04-4F48-8620-4CA2AB641934} = {F318E6F5-0343-491B-9264-CFFB4CCF1241} + {BAFCEB19-4FFC-44DF-8240-93172191080F} = {6FCD97D3-1EC8-4BB0-8BE1-245B9E51565A} + {FE628370-BD9E-4745-8C5B-EDAA44BBA2BB} = {6FCD97D3-1EC8-4BB0-8BE1-245B9E51565A} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {8EC462FD-D22E-90A8-E5CE-7E832BA40C5D} + {22755F3D-C55D-436C-9C9F-C564001B976B} = {F318E6F5-0343-491B-9264-CFFB4CCF1241} + {8199D547-23AC-4B10-9BD1-2996A6C35B1D} = {F318E6F5-0343-491B-9264-CFFB4CCF1241} + {062ADC2E-EF77-4319-9269-D60D39E31C0E} = {F318E6F5-0343-491B-9264-CFFB4CCF1241} + {65C395EC-81E9-4919-9721-72CAA3E4780D} = {F318E6F5-0343-491B-9264-CFFB4CCF1241} + {DF751DD1-9869-4916-B946-A8513A7CE706} = {F318E6F5-0343-491B-9264-CFFB4CCF1241} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {61948E36-4C2B-4BC9-80B6-9E155CE9F7DE} + EndGlobalSection +EndGlobal diff --git a/code/MessengerApi/.config/dotnet-tools.json b/code/MessengerApi/.config/dotnet-tools.json new file mode 100644 index 0000000..76ca931 --- /dev/null +++ b/code/MessengerApi/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "8.0.10", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Constants.cs b/code/MessengerApi/Constants.cs new file mode 100644 index 0000000..a3f5f2f --- /dev/null +++ b/code/MessengerApi/Constants.cs @@ -0,0 +1,7 @@ +namespace MessengerApi +{ + public class Constants + { + public const string USERFILE_FILENAME = "/app/users.conf"; + } +} \ No newline at end of file diff --git a/code/MessengerApi/Contracts/Factories/IDbContextFactory.cs b/code/MessengerApi/Contracts/Factories/IDbContextFactory.cs new file mode 100644 index 0000000..0b16e89 --- /dev/null +++ b/code/MessengerApi/Contracts/Factories/IDbContextFactory.cs @@ -0,0 +1,9 @@ +using MessengerApi.Db; + +namespace MessengerApi.Contracts.Factories +{ + public interface IDbContextFactory + { + MessengerDbContext CreateDbContext(); + } +} diff --git a/code/MessengerApi/Contracts/Models/Scoped/IUnitOfWork.cs b/code/MessengerApi/Contracts/Models/Scoped/IUnitOfWork.cs new file mode 100644 index 0000000..0992bf1 --- /dev/null +++ b/code/MessengerApi/Contracts/Models/Scoped/IUnitOfWork.cs @@ -0,0 +1,15 @@ +using MessengerApi.Db.Contracts.Repositories; + +namespace MessengerApi.Contracts.Models.Scoped +{ + public interface IUnitOfWork + { + IUserRepository Users { get; } + + IUserRouteRepository UserRoutes { get; } + + IMessageRepository Messages { get; } + + Task SaveChanges(CancellationToken ct = default); + } +} diff --git a/code/MessengerApi/Dockerfile b/code/MessengerApi/Dockerfile new file mode 100644 index 0000000..b1b00f4 --- /dev/null +++ b/code/MessengerApi/Dockerfile @@ -0,0 +1,8 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER app +WORKDIR /app +EXPOSE 8080 +COPY ./publish . +ENTRYPOINT ["dotnet", "MessengerApi.dll"] \ No newline at end of file diff --git a/code/MessengerApi/Factories/DbContextFactory.cs b/code/MessengerApi/Factories/DbContextFactory.cs new file mode 100644 index 0000000..7959e53 --- /dev/null +++ b/code/MessengerApi/Factories/DbContextFactory.cs @@ -0,0 +1,34 @@ +using MessengerApi.Configuration.Model; +using MessengerApi.Configuration.Model.Persistence; +using MessengerApi.Contracts.Factories; +using MessengerApi.Db; +using MessengerApi.Db.Npg; +using MessengerApi.Db.Sql; +using Microsoft.EntityFrameworkCore; + +namespace MessengerApi.Factories +{ + public class DbContextFactory : IDbContextFactory, IDbContextFactory + { + private readonly MessengerConfiguration configuration; + + public DbContextFactory(MessengerConfiguration configuration) + { + this.configuration = configuration; + } + + public MessengerDbContext CreateDbContext() + { + if (this.configuration.PersistenceConfiguration.PersistenceType == Configuration.Enums.PersistenceTypes.Sql) + { + return new MessengerSqlDbContext((configuration.PersistenceConfiguration as SqlPersistenceConfiguration).ConnectionString); + } + else if (this.configuration.PersistenceConfiguration.PersistenceType == Configuration.Enums.PersistenceTypes.PostgreSql) + { + return new MessengerNpgDbContext((configuration.PersistenceConfiguration as NpgPersistenceConfiguration).ConnectionString); + } + + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Factories/LoggerFactory.cs b/code/MessengerApi/Factories/LoggerFactory.cs new file mode 100644 index 0000000..8a27428 --- /dev/null +++ b/code/MessengerApi/Factories/LoggerFactory.cs @@ -0,0 +1,32 @@ +using MessengerApi.Configuration.Model; + +namespace MessengerApi.Factories +{ + public class LoggerFactory : IServiceProvider + { + private readonly MessengerConfiguration _configuration; + + public LoggerFactory(MessengerConfiguration configuration) + { + _configuration = configuration; + } + + public ILogger CreateLogger() + { + var logger = new ConsoleLogger() + { + IsDebugOutputEnabled = (this._configuration.Verbosity == Configuration.Enums.LoggingVerbosity.Debug || this._configuration.Verbosity == Configuration.Enums.LoggingVerbosity.Trace) + ? true : false, + IsTraceOutputEnabled = this._configuration.Verbosity == Configuration.Enums.LoggingVerbosity.Trace + ? true : false + }; + + return logger; + } + + public object GetService(Type serviceType) + { + return this.CreateLogger(); + } + } +} diff --git a/code/MessengerApi/GlobalUsings.cs b/code/MessengerApi/GlobalUsings.cs new file mode 100644 index 0000000..978d27f --- /dev/null +++ b/code/MessengerApi/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using portaloggy; +global using ILogger = portaloggy.ILogger; \ No newline at end of file diff --git a/code/MessengerApi/Handlers/CustomBearerAuthenticationHandler.cs b/code/MessengerApi/Handlers/CustomBearerAuthenticationHandler.cs new file mode 100644 index 0000000..b67d99a --- /dev/null +++ b/code/MessengerApi/Handlers/CustomBearerAuthenticationHandler.cs @@ -0,0 +1,81 @@ +using MessengerApi.Contracts.Models.Scoped; +using MessengerApi.Models; +using MessengerApi.Models.Scoped; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using System.Security.Claims; +using System.Text.Encodings.Web; + +namespace MessengerApi.Handlers +{ + /// + /// Validates our permananet API keys sent over as Bearer tokens. + /// + public class CustomBearerAuthenticationHandler : AuthenticationHandler + { + private readonly IMemoryCache memoryCache; + + public CustomBearerAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory loggerFactory, + UrlEncoder encoder, + IMemoryCache memoryCache) + : base(options, loggerFactory, encoder) + { + this.memoryCache = memoryCache; + } + + protected override Task HandleAuthenticateAsync() + { + const string HEADER = "Authorization"; + const string PREFIX = "Bearer "; + + Context.RequestServices.GetRequiredService(); // creates the object in scope. + + if (!Request.Headers.TryGetValue(HEADER, out var authHeader) || + !authHeader.ToString().StartsWith(PREFIX)) + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + + var token = authHeader.ToString().Substring(PREFIX.Length).Trim(); + + if(this.memoryCache.TryGetValue(token, out CachedIdentity oldCache)) + { + var identity = Context.RequestServices.GetRequiredService(); + identity.User = oldCache.User; + identity.UserRoutes = oldCache.UserRoutes; + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(oldCache.ClaimsPrincipal, Scheme.Name))); + } + else + { + var unitOfWork = Context.RequestServices.GetRequiredService(); + var user = unitOfWork.Users.SingleByApiKeyAndEnabled(Guid.Parse(token), true); + var routes = unitOfWork.UserRoutes.GetAllByUser(user).ToArray(); + + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + new List + { + new Claim(ClaimTypes.NameIdentifier, user.Name), + new Claim(ClaimTypes.Name, user.Name) + }, Scheme.Name)); + + var cache = new CachedIdentity + { + ClaimsPrincipal = principal, + User = user, + UserRoutes = routes + }; + + this.memoryCache.Set(token, cache, TimeSpan.FromMinutes(5)); + + var identity = Context.RequestServices.GetRequiredService(); + identity.User = cache.User; + identity.UserRoutes = cache.UserRoutes; + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(cache.ClaimsPrincipal, Scheme.Name))); + } + } + } +} diff --git a/code/MessengerApi/Handlers/Endpoint/AckEndpointHandler.cs b/code/MessengerApi/Handlers/Endpoint/AckEndpointHandler.cs new file mode 100644 index 0000000..a161f9f --- /dev/null +++ b/code/MessengerApi/Handlers/Endpoint/AckEndpointHandler.cs @@ -0,0 +1,40 @@ +using MessengerApi.Contracts.Models.Scoped; +using MessengerApi.Models.Scoped; + +namespace MessengerApi.Handlers.Endpoint +{ + public class AckEndpointHandler + { + private readonly ILogger logger; + private readonly IUnitOfWork unitOfWork; + private readonly Identity identity; + + public AckEndpointHandler( + ILogger logger, + IUnitOfWork unitOfWork, + Identity identity) + { + this.logger = logger; + this.unitOfWork = unitOfWork; + this.identity = identity; + } + + public async Task AckMessage(Guid messageId) + { + var message = unitOfWork.Messages.GetById(messageId); + + // Authorize. + if (message.ToId != this.identity.User.Id) + { + throw new InvalidOperationException("It's not your message to ack."); + } + else if(!message.IsDelivered) + { + throw new InvalidOperationException("Can't ack undelivered message."); + } + + // Act. + message.IsAcknowledged = true; + } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Handlers/Endpoint/PeekEndpointHandler.cs b/code/MessengerApi/Handlers/Endpoint/PeekEndpointHandler.cs new file mode 100644 index 0000000..b3f08cf --- /dev/null +++ b/code/MessengerApi/Handlers/Endpoint/PeekEndpointHandler.cs @@ -0,0 +1,35 @@ +using MessengerApi.Contracts.Models.Scoped; +using MessengerApi.Models.Scoped; + +namespace MessengerApi.Handlers.Endpoint +{ + public class PeekEndpointHandler + { + private readonly ILogger logger; + + private readonly Timing timing; + private readonly Identity identity; + private readonly IUnitOfWork unitOfWork; + + public PeekEndpointHandler( + ILogger logger, + Timing timing, + Identity identity, + IUnitOfWork unitOfWork) + { + this.logger = logger; + this.timing = timing; + this.identity = identity; + this.unitOfWork = unitOfWork; + } + + public Task Peek() + { + var pendingMessages = this.unitOfWork.Messages.GetPendingMessages(this.identity.User); + + this.logger.Debug($"[{this.timing.Timestamp:s}] User {this.identity.User.Name} is receiving {pendingMessages.Count()}."); + + return Task.FromResult(pendingMessages.Count()); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Handlers/Endpoint/ReceiveEndpointHandler.cs b/code/MessengerApi/Handlers/Endpoint/ReceiveEndpointHandler.cs new file mode 100644 index 0000000..08f0f38 --- /dev/null +++ b/code/MessengerApi/Handlers/Endpoint/ReceiveEndpointHandler.cs @@ -0,0 +1,43 @@ +using MessengerApi.Contracts.Models.Scoped; +using MessengerApi.Db.Entities; +using MessengerApi.Models.Scoped; + +namespace MessengerApi.Handlers.Endpoint +{ + public class ReceiveEndpointHandler + { + private readonly ILogger logger; + + private readonly Timing timing; + private readonly Identity identity; + private readonly IUnitOfWork unitOfWork; + + public ReceiveEndpointHandler( + ILogger logger, + Timing timing, + Identity identity, + IUnitOfWork unitOfWork) + { + this.logger = logger; + this.timing = timing; + this.identity = identity; + this.unitOfWork = unitOfWork; + } + + public Task ReceiveMessages() + { + var pendingMessages = this.unitOfWork.Messages.GetPendingMessages(this.identity.User); + + this.logger.Debug($"[{this.timing.Timestamp:s}] User {this.identity.User.Name} is receiving {pendingMessages.Count()}."); + + if (!pendingMessages.Any()) + { + return Task.FromResult(new Message[0]); + } + + var messages = pendingMessages.ToList(); + messages.ForEach(x => x.IsDelivered = true); + return Task.FromResult(messages.ToArray()); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Handlers/Endpoint/SendEndpointHandler.cs b/code/MessengerApi/Handlers/Endpoint/SendEndpointHandler.cs new file mode 100644 index 0000000..f6e786a --- /dev/null +++ b/code/MessengerApi/Handlers/Endpoint/SendEndpointHandler.cs @@ -0,0 +1,60 @@ +using MessengerApi.Configuration.Model; +using MessengerApi.Contracts.Models.Scoped; +using MessengerApi.Db.Entities; +using MessengerApi.Models.Scoped; + +namespace MessengerApi.Handlers.Endpoint +{ + public class SendEndpointHandler + { + private readonly MessengerConfiguration configuration; + private readonly ILogger logger; + + private readonly Timing timing; + private readonly Identity identity; + private readonly IUnitOfWork unitOfWork; + + public SendEndpointHandler( + MessengerConfiguration configuration, + ILogger logger, + IUnitOfWork unitOfWork, + Timing timing, + Identity identity) + { + this.configuration = configuration; + this.logger = logger; + this.unitOfWork = unitOfWork; + this.timing = timing; + this.identity = identity; + } + + public Task SendMessage( + Guid? toUserId, + string payload, + string payloadType, + int? payloadLifespanInSeconds) + { + // Authorize. + var targetRecipientId = toUserId.HasValue + ? this.identity.UserRoutes.Single(x => x.From.Id == this.identity.User.Id && x.To.Id == toUserId.Value).To.Id + : this.identity.UserRoutes.Single().To.Id; + + this.logger.Debug($"[{this.timing.Timestamp:s}] User {this.identity.User.Name} is authorized to send message to {targetRecipientId}."); + + // Act. + var message = new Message + { + Id = Guid.NewGuid(), + CreatedUtc = this.timing.Timestamp, + FromId = this.identity.User.Id, + ToId = targetRecipientId, + Payload = payload, + PayloadType = payloadType, + PayloadLifespanInSeconds = payloadLifespanInSeconds ?? (this.configuration.DefaultMessageLifetimeInMinutes * 60) + }; + + this.unitOfWork.Messages.Add(message); + return Task.FromResult(message); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Handlers/HousekeepingHandler.cs b/code/MessengerApi/Handlers/HousekeepingHandler.cs new file mode 100644 index 0000000..4c48f56 --- /dev/null +++ b/code/MessengerApi/Handlers/HousekeepingHandler.cs @@ -0,0 +1,47 @@ +using MessengerApi.Configuration.Model; +using MessengerApi.Contracts.Factories; +using Microsoft.EntityFrameworkCore; + +namespace MessengerApi.Handlers +{ + public class HousekeepingHandler + { + private readonly ILogger logger; + private readonly MessengerConfiguration configuration; + private readonly IDbContextFactory dbContextFactory; + + public HousekeepingHandler( + ILogger logger, + IDbContextFactory dbContextFactory, + MessengerConfiguration configuration) + { + this.logger = logger; + this.dbContextFactory = dbContextFactory; + this.configuration = configuration; + } + + public async Task RemoveOldMessages() + { + this.logger.Trace($"Executing {nameof(this.RemoveOldMessages)}."); + + var timestamp = DateTime.UtcNow; + var cutoff = timestamp.AddMinutes(-this.configuration.HousekeepingMessageAgeInMinutes); + using var ctx = this.dbContextFactory.CreateDbContext(); + await ctx.Messages.Where(x => x.CreatedUtc < cutoff).ExecuteDeleteAsync(); + + if (this.configuration.HousekeepingMessageState != Configuration.Enums.HousekeepingMessageStates.None) + { + this.logger.Trace($"Executing additional message state cleaning in {nameof(this.RemoveOldMessages)}."); + + if (this.configuration.HousekeepingMessageState == Configuration.Enums.HousekeepingMessageStates.Delivered) + { + await ctx.Messages.Where(x => x.IsDelivered).ExecuteDeleteAsync(); + } + else if (this.configuration.HousekeepingMessageState == Configuration.Enums.HousekeepingMessageStates.Acknowledged) + { + await ctx.Messages.Where(x => x.IsAcknowledged).ExecuteDeleteAsync(); + } + } + } + } +} diff --git a/code/MessengerApi/Handlers/UserSetupHandler.cs b/code/MessengerApi/Handlers/UserSetupHandler.cs new file mode 100644 index 0000000..4148995 --- /dev/null +++ b/code/MessengerApi/Handlers/UserSetupHandler.cs @@ -0,0 +1,99 @@ +using MessengerApi.Configuration.Model; +using MessengerApi.Contracts.Factories; +using MessengerApi.Db; +using MessengerApi.Models; +using System.Text; + +namespace MessengerApi.Handlers +{ + // TODO: This needs to be redone, because at every run, it wipes users and creates new ones. This makes + // all existing DB messages unassignable. + public class UserSetupHandler + { + private readonly MessengerConfiguration configuration; + private readonly ILogger logger; + private readonly IDbContextFactory dbContextFactory; + + public UserSetupHandler( + MessengerConfiguration configuration, + ILogger logger, + IDbContextFactory dbContextFactory) + { + this.configuration = configuration; + this.logger = logger; + this.dbContextFactory = dbContextFactory; + } + + public async Task UpdateFromFile(FileInfo file) + { + if(file.Exists) + { + var lines = await File.ReadAllLinesAsync(file.FullName, Encoding.UTF8); + var items = await this.ReadLines(lines); + + await this.SynchronizeUsers(items); + } + } + + private async Task ReadLines(string[] lines) + { + var items = new List(); + + foreach (var line in lines) + { + var values = line.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var item = new UserSetupItem + { + UserName = values[0], + ApiKey = values[1], + }; + + if(values.Length > 2) + { + item.CanSendToUserNames = values[2].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + items.Add(item); + } + + if (items.GroupBy(x => x.UserName).Any(x => x.Count() > 1)) + { + throw new InvalidOperationException("Usernames are not unique. One username per line."); + } + else if(items.GroupBy(x=>x.ApiKey).Any(x=>x.Count() > 1)) + { + throw new InvalidOperationException("API keys are not unique. One API key per line."); + } + + return items.ToArray(); + } + + private Task SynchronizeUsers(IEnumerable users) + { + using var db = this.dbContextFactory.CreateDbContext(); + db.RemoveRange(db.Users); + db.RemoveRange(db.UserRoutes); + + var dbUsers = users.Select(x => new Db.Entities.User + { + Id = new Guid(), + Name = x.UserName, + ApiKey = Guid.Parse(x.ApiKey), + IsEnabled = true + }); + + var dbRoutes = users.SelectMany(x => x.CanSendToUserNames.Select(cs => new Db.Entities.UserRoute + { + Id = new Guid(), + From = dbUsers.Single(dbu => dbu.Name == x.UserName), + To = dbUsers.Single(dbu => dbu.Name == x.UserName) + })); + + db.AddRange(dbUsers); + db.AddRange(dbRoutes); + + db.SaveChanges(); + return Task.CompletedTask; + } + } +} diff --git a/code/MessengerApi/MessengerApi.csproj b/code/MessengerApi/MessengerApi.csproj new file mode 100644 index 0000000..16f652b --- /dev/null +++ b/code/MessengerApi/MessengerApi.csproj @@ -0,0 +1,30 @@ + + + + net9.0 + disable + enable + 85c81e87-1274-45ce-8b91-6d6619ffdfa2 + Linux + ..\out\ + MessengerApi.Api.Program + true + + + + + + + + + + + + + + + + + + + diff --git a/code/MessengerApi/Models/CachedIdentity.cs b/code/MessengerApi/Models/CachedIdentity.cs new file mode 100644 index 0000000..adc676d --- /dev/null +++ b/code/MessengerApi/Models/CachedIdentity.cs @@ -0,0 +1,13 @@ +using System.Security.Claims; + +namespace MessengerApi.Models +{ + public class CachedIdentity + { + public Db.Entities.User User { get; set; } + + public Db.Entities.UserRoute[] UserRoutes { get; set; } + + public ClaimsPrincipal ClaimsPrincipal { get; set; } + } +} diff --git a/code/MessengerApi/Models/Http/AckRequest.cs b/code/MessengerApi/Models/Http/AckRequest.cs new file mode 100644 index 0000000..ccc7aa9 --- /dev/null +++ b/code/MessengerApi/Models/Http/AckRequest.cs @@ -0,0 +1,7 @@ +namespace MessengerApi.Models.Http +{ + public class AckRequest + { + public Guid MessageId { get; set; } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Models/Http/SendRequest.cs b/code/MessengerApi/Models/Http/SendRequest.cs new file mode 100644 index 0000000..42896d1 --- /dev/null +++ b/code/MessengerApi/Models/Http/SendRequest.cs @@ -0,0 +1,13 @@ +namespace MessengerApi.Models.Http +{ + public class SendRequest + { + public Guid? ToUserId { get; set; } + + public string Payload { get; set; } + + public string PayloadType { get; set; } + + public int? PayloadLifetimeInSeconds { get; set; } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Models/Http/VerifyRequest.cs b/code/MessengerApi/Models/Http/VerifyRequest.cs new file mode 100644 index 0000000..b0c6f72 --- /dev/null +++ b/code/MessengerApi/Models/Http/VerifyRequest.cs @@ -0,0 +1,7 @@ +namespace MessengerApi.Models.Http +{ + public class VerifyRequest + { + public Guid MessageId { get; set; } + } +} diff --git a/code/MessengerApi/Models/Scoped/Identity.cs b/code/MessengerApi/Models/Scoped/Identity.cs new file mode 100644 index 0000000..0c8370b --- /dev/null +++ b/code/MessengerApi/Models/Scoped/Identity.cs @@ -0,0 +1,9 @@ +namespace MessengerApi.Models.Scoped +{ + public class Identity + { + public Db.Entities.User User { get; set; } + + public Db.Entities.UserRoute[] UserRoutes { get; set; } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Models/Scoped/Timing.cs b/code/MessengerApi/Models/Scoped/Timing.cs new file mode 100644 index 0000000..faecb26 --- /dev/null +++ b/code/MessengerApi/Models/Scoped/Timing.cs @@ -0,0 +1,7 @@ +namespace MessengerApi.Models.Scoped +{ + public class Timing + { + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + } +} diff --git a/code/MessengerApi/Models/Scoped/UnitOfWork.cs b/code/MessengerApi/Models/Scoped/UnitOfWork.cs new file mode 100644 index 0000000..abfe242 --- /dev/null +++ b/code/MessengerApi/Models/Scoped/UnitOfWork.cs @@ -0,0 +1,33 @@ +using MessengerApi.Contracts.Factories; +using MessengerApi.Contracts.Models.Scoped; +using MessengerApi.Db; +using MessengerApi.Db.Contracts.Repositories; +using MessengerApi.Db.Repositories; + +namespace MessengerApi.Models.Scoped +{ + public class UnitOfWork : IUnitOfWork + { + private MessengerDbContext context; + + public IUserRepository Users { get; } + + public IUserRouteRepository UserRoutes { get; } + + public IMessageRepository Messages { get; } + + public UnitOfWork( + IDbContextFactory dbContextFactory) + { + this.context = dbContextFactory.CreateDbContext(); + this.Users = new UserRepository(this.context.Users); + this.UserRoutes = new UserRouteRepository(this.context.UserRoutes); + this.Messages = new MessageRepository(this.context.Messages); + } + + public Task SaveChanges(CancellationToken ct = default) + { + return this.context.SaveChangesAsync(ct); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Models/UserSetupItem.cs b/code/MessengerApi/Models/UserSetupItem.cs new file mode 100644 index 0000000..2aad706 --- /dev/null +++ b/code/MessengerApi/Models/UserSetupItem.cs @@ -0,0 +1,11 @@ +namespace MessengerApi.Models +{ + public class UserSetupItem + { + public string UserName { get; set; } + + public string ApiKey { get; set; } + + public string[] CanSendToUserNames { get; set; } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Program.cs b/code/MessengerApi/Program.cs new file mode 100644 index 0000000..f76f268 --- /dev/null +++ b/code/MessengerApi/Program.cs @@ -0,0 +1,313 @@ +using MessengerApi.Configuration.Model; +using MessengerApi.Configuration.Model.Persistence; +using MessengerApi.Configuration.Sources.Environment; +using MessengerApi.Contracts.Factories; +using MessengerApi.Contracts.Models.Scoped; +using MessengerApi.Db; +using MessengerApi.Factories; +using MessengerApi.Handlers; +using MessengerApi.Handlers.Endpoint; +using MessengerApi.Models.Http; +using MessengerApi.Models.Scoped; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Net; +using System.Threading.RateLimiting; + +namespace MessengerApi.Api +{ + public class Program + { + public static void Main(string[] args) + { + MessengerConfiguration configuration = null; + + try + { + configuration = new MessengerConfiguration(new EnvironmentConfigurationSource()); + } + catch (Exception ex) + { + Console.WriteLine("Can't load settings.", ex); + throw; + } + + var builder = WebApplication.CreateBuilder(args); + + builder.Configuration.AddEnvironmentVariables(); + + builder.Services.AddMemoryCache(); + builder.Services.AddSingleton(configuration); + builder.Services.AddSingleton(new Factories.LoggerFactory(configuration).CreateLogger()); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // Authentication. + builder.Services + .AddAuthentication("Bearer") + .AddScheme("Bearer", null); + + // CORS. + builder.Services + .AddCors(opt => opt.AddPolicy("originpolicy", builder => + { + builder + .WithOrigins(configuration.Origins.ToArray()) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + })); + + // Ratelimiting + builder.Services.AddRateLimiter(options => + { + options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => + { + var key = httpContext.Request.Headers["Authorization"].FirstOrDefault() + ?? "anonymous"; + + return RateLimitPartition.GetFixedWindowLimiter(key, _ => new FixedWindowRateLimiterOptions + { + PermitLimit = configuration.RateLimitPerMinute, + Window = TimeSpan.FromMinutes(1), + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 0 + }); + }); + + options.RejectionStatusCode = 429; + }); + + // Proxy registration to forward real client IPs. + builder.Services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + + foreach (var proxy in configuration.Proxies) + { + options.KnownProxies.Add(IPAddress.Parse(proxy)); + } + }); + + var app = builder.Build(); + app.UseDeveloperExceptionPage(); + + // DB Migrations + using (var ctx = app.Services.GetRequiredService().CreateDbContext()) + { + var migrationLogger = app.Services.GetRequiredService(); + + try + { + if (ctx.Database.GetPendingMigrations().Any()) + { + migrationLogger.Info("Applying migrations."); + ctx.Database.Migrate(); + } + else + { + migrationLogger.Info("No migrations pending."); + } + } + catch (Exception ex) + { + migrationLogger.Error("Can't run migrations successfully.", ex); + throw; + } + } + + // Housekeeping. + if (configuration.HousekeepingEnabled) + { + _ = Task.Run(async () => + { + while (true) + { + await app.Services.GetService().RemoveOldMessages(); + await Task.Delay(TimeSpan.FromMinutes(1)); + } + }); + } + + // User synchronization + var userSetupHandler = app.Services.GetRequiredService(); + userSetupHandler.UpdateFromFile(new FileInfo(Constants.USERFILE_FILENAME)).GetAwaiter().GetResult(); + + app.UseStaticFiles(); + app.UseRouting(); + app.UseAuthentication(); + app.UseCors("originpolicy"); + app.UseForwardedHeaders(); + + // Ray id logging. + app.Use(async (context, next) => + { + var stamp = DateTime.UtcNow; + var logger = context.RequestServices.GetRequiredService(); + var ipa = context?.Connection?.RemoteIpAddress?.ToString() ?? "unknown"; + var uid = context?.User?.Identity?.Name ?? "unknown"; + var una = context?.User?.Claims?.SingleOrDefault(x => x.Type == "UserName")?.Value ?? "unknown"; + var rid = context?.TraceIdentifier ?? "unknown"; + var endpoint = context?.GetEndpoint()?.DisplayName ?? "unknown"; + + logger.Info($"{endpoint} call {rid}; ip {ipa}; u {una}/{uid}"); + + await next(); + }); + + app.UseRateLimiter(); + + // Endpoint registration. + app.MapPost("/send", async ( + ILogger logger, + IUnitOfWork unitOfWork, + SendEndpointHandler handler, + [FromBody] SendRequest request) => + { + try + { + var response = await handler.SendMessage(request.ToUserId, request.Payload, request.PayloadType, request.PayloadLifetimeInSeconds); + await unitOfWork.SaveChanges(); + return Results.Json(response.Id); + } + catch (Exception ex) + { + logger.Error("Can't send.", ex); + return Results.InternalServerError(); + } + }); + + app.MapGet("/receive", async ( + ILogger logger, + IUnitOfWork unitOfWork, + ReceiveEndpointHandler handler) => + { + try + { + var messages = await handler.ReceiveMessages(); + + if (messages?.Any() != true) + { + return Results.NoContent(); + } + else + { + await unitOfWork.SaveChanges(); + + return Results.Json(new + { + Messages = messages.Select(x => new + { + Id = x.Id, + TimestampUtc = x.CreatedUtc, + Payload = x.Payload, + PayloadType = x.PayloadType, + Sender = x.FromId + }) + }); + } + } + catch (Exception ex) + { + logger.Error("Can't send.", ex); + return Results.InternalServerError(); + } + }); + + app.MapPost("/ack", async ( + ILogger logger, + IUnitOfWork unitOfWork, + AckEndpointHandler handler, + AckRequest request) => + { + try + { + await handler.AckMessage(request.MessageId); + await unitOfWork.SaveChanges(); + return Results.Ok(); + } + catch (Exception ex) + { + logger.Error("Can't send.", ex); + return Results.InternalServerError(); + } + }); + + app.MapGet("/yellowpages", ( + ILogger logger, + IUnitOfWork unitOfWork, + Identity identity) => + { + try + { + var routes = unitOfWork.UserRoutes.GetByFrom(identity.User).ToList(); + return Results.Json(new + { + Users = routes.Select(x => new + { + Id = x.To.Id, + Name = x.To.Name + }) + }); + } + catch (Exception ex) + { + logger.Error("Can't yellowpages.", ex); + return Results.InternalServerError(); + } + }); + + app.MapGet("/peek", async ( + ILogger logger, + PeekEndpointHandler handler) => + { + try + { + var pending = await handler.Peek(); + return Results.Json(pending); + } + catch (Exception ex) + { + logger.Error("Can't peek.", ex); + return Results.InternalServerError(); + } + }); + + app.MapGet("/verify", ( + ILogger logger, + IUnitOfWork unitOfWork, + Guid messageId) => + { + try + { + var message = unitOfWork.Messages.GetById(messageId); + + return Results.Json(new + { + message.IsDelivered, + message.IsAcknowledged + }); + } + catch (Exception ex) + { + logger.Error("Can't verify.", ex); + return Results.InternalServerError(); + } + }); + + app.Run(); + } + } +} \ No newline at end of file diff --git a/code/MessengerApi/Properties/launchSettings.json b/code/MessengerApi/Properties/launchSettings.json new file mode 100644 index 0000000..c29747b --- /dev/null +++ b/code/MessengerApi/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "PERSISTENCE_TYPE": "Sql", + "CORS_ORIGINS": "", + "PROXIES": "", + "QUERY_RATE_PER_MINUTE": "100", + "DEFAULT_MESSAGE_LIFETIME_IN_MINUTES": "60", + "HOUSEKEEPING_ENABLED": "False", + "LOGGING_VERBOSITY": "Trace" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5259" + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55327", + "sslPort": 44348 + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0922b77 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + messengerapi: + image: https://gitea.masita.net/mc/messengerapi:latest + container_name: messengerapi + restart: unless-stopped + environment: + - ASPNETCORE_ENVIRONMENT=Production \ No newline at end of file