Initial commit carried over from private repo. This is V2.
This commit is contained in:
26
.gitea/workflows/build.yml
Normal file
26
.gitea/workflows/build.yml
Normal file
@ -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
|
||||||
23
.gitea/workflows/docker-build-and-push.yml
Normal file
23
.gitea/workflows/docker-build-and-push.yml
Normal file
@ -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
|
||||||
12
Directory.Packages.props
Normal file
12
Directory.Packages.props
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<Project>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6" />
|
||||||
|
<PackageVersion Include="portaloggy" Version="1.0.2" />
|
||||||
|
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" />
|
||||||
|
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.1.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@ -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"]
|
||||||
10
LICENSE
Normal file
10
LICENSE
Normal file
@ -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 <http://unlicense.org/>
|
||||||
15
NuGet.config
Normal file
15
NuGet.config
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
<add key="mc" value="https://gitea.masita.net/api/packages/mc/nuget/index.json" />
|
||||||
|
</packageSources>
|
||||||
|
<packageSourceMapping>
|
||||||
|
<packageSource key="nuget.org">
|
||||||
|
<package pattern="*" />
|
||||||
|
</packageSource>
|
||||||
|
<packageSource key="mc">
|
||||||
|
<package pattern="portaloggy" />
|
||||||
|
</packageSource>
|
||||||
|
</packageSourceMapping>
|
||||||
|
</configuration>
|
||||||
129
README.md
Normal file
129
README.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# messengerapi
|
||||||
|
|
||||||
|
[](https://paypal.me/emsicz) [](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.
|
||||||
BIN
assets/why.jpg
Normal file
BIN
assets/why.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
@ -0,0 +1,9 @@
|
|||||||
|
namespace MessengerApi.Configuration.Enums
|
||||||
|
{
|
||||||
|
public enum HousekeepingMessageStates
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Delivered,
|
||||||
|
Acknowledged
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace MessengerApi.Configuration.Enums
|
||||||
|
{
|
||||||
|
public enum LoggingVerbosity
|
||||||
|
{
|
||||||
|
Normal,
|
||||||
|
Debug,
|
||||||
|
Trace
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
namespace MessengerApi.Configuration.Enums
|
||||||
|
{
|
||||||
|
public enum PersistenceTypes
|
||||||
|
{
|
||||||
|
Sql,
|
||||||
|
PostgreSql
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// CORS origins.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Origins { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of proxies that are trusted to provide forwarding headers.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Proxies { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persistence layer configs (database).
|
||||||
|
/// </summary>
|
||||||
|
public PersistenceConfiguration PersistenceConfiguration { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Limits rate of user calls to not DoS the service.
|
||||||
|
/// </summary>
|
||||||
|
public int RateLimitPerMinute { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message lifetime unless set differently in message body.
|
||||||
|
/// </summary>
|
||||||
|
public int DefaultMessageLifetimeInMinutes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, messages are periodically wiped to free up space.
|
||||||
|
/// </summary>
|
||||||
|
public bool HousekeepingEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Messages older than value set will be deleted regardless of their delivery state.
|
||||||
|
/// </summary>
|
||||||
|
public int HousekeepingMessageAgeInMinutes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines level of log messages displayed.
|
||||||
|
/// </summary>
|
||||||
|
public LoggingVerbosity Verbosity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In addition to <see cref="HousekeepingMessageAgeInMinutes"/> messages of certain state can also be deleted, increasing storage efficiency.
|
||||||
|
/// </summary>
|
||||||
|
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<string>(Env.CORS_ORIGINS)),
|
||||||
|
EnvironmentPersistenceConfigurationParser.Parse(config))
|
||||||
|
{
|
||||||
|
Populate<string>(config, Env.PROXIES, x => this.Proxies = ProxiesParser.Parse(x));
|
||||||
|
Populate<int>(config, Env.QUERY_RATE_PER_MINUTE, x => this.RateLimitPerMinute = x);
|
||||||
|
Populate<int>(config, Env.DEFAULT_MESSAGE_LIFETIME_IN_MINUTES, x => this.DefaultMessageLifetimeInMinutes = x);
|
||||||
|
Populate<bool>(config, Env.HOUSEKEEPING_ENABLED, x => this.HousekeepingEnabled = x);
|
||||||
|
Populate<int>(config, Env.HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES, x => this.HousekeepingMessageAgeInMinutes = x);
|
||||||
|
Populate<string>(config, Env.HOUSEKEEPING_MESSAGE_STATE, x => this.HousekeepingMessageState = HousekeepingMessageStateParser.Parse(x));
|
||||||
|
Populate<string>(config, Env.LOGGING_VERBOSITY, x => this.Verbosity = LoggingVerbosityParser.Parse(x));
|
||||||
|
|
||||||
|
void Populate<T>(IEnvironmentConfigurationSource config, string key, Action<T> set)
|
||||||
|
{
|
||||||
|
if (config.HasKey(key))
|
||||||
|
{
|
||||||
|
var value = config.GetValue<T>(key);
|
||||||
|
set(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
using MessengerApi.Configuration.Enums;
|
||||||
|
|
||||||
|
namespace MessengerApi.Configuration.Model.Persistence.Base
|
||||||
|
{
|
||||||
|
public abstract class PersistenceConfiguration
|
||||||
|
{
|
||||||
|
public abstract PersistenceTypes PersistenceType { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<string>(Constants.EnvironmentVariables.NPG_CONNECTIONSTRING)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<string>(Constants.EnvironmentVariables.SQL_CONNECTIONSTRING)) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
code/MessengerApi.Configuration/Parsers/CorsParser.cs
Normal file
11
code/MessengerApi.Configuration/Parsers/CorsParser.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<string>(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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
code/MessengerApi.Configuration/Parsers/ProxiesParser.cs
Normal file
11
code/MessengerApi.Configuration/Parsers/ProxiesParser.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<T>(string key)
|
||||||
|
{
|
||||||
|
return (T)Convert.ChangeType(System.Environment.GetEnvironmentVariable(key), typeof(T));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
namespace MessengerApi.Configuration.Sources.Environment
|
||||||
|
{
|
||||||
|
public interface IEnvironmentConfigurationSource : IConfigurationSource
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
namespace MessengerApi.Configuration.Sources
|
||||||
|
{
|
||||||
|
public interface IConfigurationSource
|
||||||
|
{
|
||||||
|
bool HasKey(string key);
|
||||||
|
|
||||||
|
T GetValue<T>(string key);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
code/MessengerApi.Contracts.MessageParser/IMessageParser.cs
Normal file
22
code/MessengerApi.Contracts.MessageParser/IMessageParser.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
namespace MessengerApi.Contracts.MessageParser
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A tool that helps converting POCO request/response models into <see cref="InboxMessage"/>, <see cref="OutboxMessage"/> and back.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If you implement this, it's gonna be a lot easier for you to translate
|
||||||
|
/// dumb request class into <see cref="OutboxMessage"/>, then convert
|
||||||
|
/// <see cref="InboxMessage"/> back to request at server-side, and do the
|
||||||
|
/// same with the response all the way down to the client.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IMessageParser<TRequest, TResponse>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
code/MessengerApi.Contracts.MessageParser/MessageParser.cs
Normal file
50
code/MessengerApi.Contracts.MessageParser/MessageParser.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace MessengerApi.Contracts.MessageParser
|
||||||
|
{
|
||||||
|
public class MessageParser<TRequest, TResponse> : IMessageParser<TRequest, TResponse>
|
||||||
|
{
|
||||||
|
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<TRequest>(message.Payload);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TResponse GetResponseFromMessage(InboxMessage message)
|
||||||
|
{
|
||||||
|
var request = JsonConvert.DeserializeObject<TResponse>(message.Payload);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
<AssemblyVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</AssemblyVersion>
|
||||||
|
<PackageVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</PackageVersion>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
<BaseOutputPath>..\out\</BaseOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MessengerApi.Contracts" Version="2025.6.28.2202" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
30
code/MessengerApi.Contracts/Client/IMessengerClient.cs
Normal file
30
code/MessengerApi.Contracts/Client/IMessengerClient.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
namespace MessengerApi.Contracts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exists for mocking reason. This is implemented by <see cref="MessengerClient"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMessengerClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Receives pending messages from the messenger API.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials">Credentials to the API.</param>
|
||||||
|
IEnumerable<InboxMessage> GetMessages();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acknowledges message reception to the server.
|
||||||
|
/// </summary>
|
||||||
|
void AckMessage(InboxMessage message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials">Credentials to the API.</param>
|
||||||
|
void SendMessage(OutboxMessage outboxMessage);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns user ids for allowed message recipients.
|
||||||
|
/// </summary>
|
||||||
|
Contact[] GetYellowPages();
|
||||||
|
}
|
||||||
|
}
|
||||||
150
code/MessengerApi.Contracts/Client/MessengerClient.cs
Normal file
150
code/MessengerApi.Contracts/Client/MessengerClient.cs
Normal file
@ -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<InboxMessage> 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<InboxMessage>().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonNode.Parse(responseContent);
|
||||||
|
var messages = new List<InboxMessage>();
|
||||||
|
|
||||||
|
foreach (var item in json["messages"].AsArray())
|
||||||
|
{
|
||||||
|
if (item["id"].GetValue<int>() == -1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.Add(new InboxMessage
|
||||||
|
{
|
||||||
|
Id = item["id"].GetValue<Guid>(),
|
||||||
|
Payload = item["payload"].ToJsonString(),
|
||||||
|
PayloadId = item["payloadId"].ToJsonString(),
|
||||||
|
PayloadType = item["payloadType"].ToJsonString(),
|
||||||
|
Sender = item["sender"].GetValue<Guid>(),
|
||||||
|
SenderTimestamp = item["senderTimestamp"].GetValue<DateTime>()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_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<Guid>(outboxMessage.ToUserId.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(outboxMessage.Payload != null)
|
||||||
|
{
|
||||||
|
body.Add("payload", JsonValue.Create<string>(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<Guid>(),
|
||||||
|
Name = x["name"].GetValue<string>()
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
return contacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AckMessage(InboxMessage message)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
code/MessengerApi.Contracts/Contact.cs
Normal file
9
code/MessengerApi.Contracts/Contact.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace MessengerApi.Contracts
|
||||||
|
{
|
||||||
|
public class Contact
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
15
code/MessengerApi.Contracts/Credentials.cs
Normal file
15
code/MessengerApi.Contracts/Credentials.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
code/MessengerApi.Contracts/Messages/InboxMessage.cs
Normal file
20
code/MessengerApi.Contracts/Messages/InboxMessage.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
namespace MessengerApi.Contracts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Message when received is inbox. For server apps, this is request-type of message. For clients, this is a response-type of message.
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
20
code/MessengerApi.Contracts/Messages/OutboxMessage.cs
Normal file
20
code/MessengerApi.Contracts/Messages/OutboxMessage.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
namespace MessengerApi.Contracts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Outbox type of message. A server-app will treat this as a response. A client app will treat this as a request.
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
18
code/MessengerApi.Contracts/MessengerApi.Contracts.csproj
Normal file
18
code/MessengerApi.Contracts/MessengerApi.Contracts.csproj
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
|
<PackageOutputPath>$(OutputPath)</PackageOutputPath>
|
||||||
|
<AssemblyVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</AssemblyVersion>
|
||||||
|
<PackageVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</PackageVersion>
|
||||||
|
<BaseOutputPath>..\out\</BaseOutputPath>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="portaloggy" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
11
code/MessengerApi.Db.Contracts/Entities/IEntity.cs
Normal file
11
code/MessengerApi.Db.Contracts/Entities/IEntity.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace MessengerApi.Db.Contracts.Entities
|
||||||
|
{
|
||||||
|
public interface IEntity
|
||||||
|
{
|
||||||
|
Guid Id { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IEntity<T> : IEntity where T : class, IEntity
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
25
code/MessengerApi.Db.Contracts/Entities/Message.cs
Normal file
25
code/MessengerApi.Db.Contracts/Entities/Message.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using MessengerApi.Db.Contracts.Entities;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Entities
|
||||||
|
{
|
||||||
|
public class Message : IEntity<Message>
|
||||||
|
{
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
15
code/MessengerApi.Db.Contracts/Entities/User.cs
Normal file
15
code/MessengerApi.Db.Contracts/Entities/User.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using MessengerApi.Db.Contracts.Entities;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Entities
|
||||||
|
{
|
||||||
|
public class User : IEntity<User>
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public Guid ApiKey { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
16
code/MessengerApi.Db.Contracts/Entities/UserRoute.cs
Normal file
16
code/MessengerApi.Db.Contracts/Entities/UserRoute.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using MessengerApi.Db.Contracts.Entities;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Entities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Describes allowed message route (who can message whom).
|
||||||
|
/// </summary>
|
||||||
|
public class UserRoute : IEntity<UserRoute>
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public User From { get; set; }
|
||||||
|
|
||||||
|
public User To { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
using MessengerApi.Db.Entities;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Contracts.Repositories
|
||||||
|
{
|
||||||
|
public interface IMessageRepository : IRepository<Message>
|
||||||
|
{
|
||||||
|
IEnumerable<Message> GetPendingMessages(User user);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
code/MessengerApi.Db.Contracts/Repositories/IRepository.cs
Normal file
15
code/MessengerApi.Db.Contracts/Repositories/IRepository.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using MessengerApi.Db.Contracts.Entities;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Contracts.Repositories
|
||||||
|
{
|
||||||
|
public interface IRepository
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IRepository<T> : IRepository where T : class, IEntity<T>
|
||||||
|
{
|
||||||
|
void Add(T entity);
|
||||||
|
|
||||||
|
T GetById(Guid id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
using MessengerApi.Db.Entities;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Contracts.Repositories
|
||||||
|
{
|
||||||
|
public interface IUserRepository : IRepository<User>
|
||||||
|
{
|
||||||
|
User SingleByApiKeyAndEnabled(Guid id, bool enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
using MessengerApi.Db.Entities;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Contracts.Repositories
|
||||||
|
{
|
||||||
|
public interface IUserRouteRepository:IRepository<UserRoute>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all routes for given user.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<UserRoute> GetAllByUser(User sender);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns routes where given user is sender.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<UserRoute> GetByFrom(User user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
using MessengerApi.Db.Npg;
|
||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Sql.Migrator
|
||||||
|
{
|
||||||
|
public partial class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<MessengerNpgDbContext>
|
||||||
|
{
|
||||||
|
public MessengerNpgDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
return new MessengerNpgDbContext(this.ConnectionString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MessengerApi.Db.Npg\MessengerApi.Db.Npg.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
16
code/MessengerApi.Db.Npg.Migrator/Program.cs
Normal file
16
code/MessengerApi.Db.Npg.Migrator/Program.cs
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
code/MessengerApi.Db.Npg/MessengerApi.Db.Npg.csproj
Normal file
17
code/MessengerApi.Db.Npg/MessengerApi.Db.Npg.csproj
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MessengerApi.Db\MessengerApi.Db.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
31
code/MessengerApi.Db.Npg/MessengerNpgDbContext.cs
Normal file
31
code/MessengerApi.Db.Npg/MessengerNpgDbContext.cs
Normal file
@ -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<Message>().HasIndex(e => new { e.ToId, e.IsDelivered }).IsUnique(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.Designer.cs
generated
Normal file
123
code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.Designer.cs
generated
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("FromId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAcknowledged")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDelivered")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Payload")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("PayloadLifespanInSeconds")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("PayloadType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("ToId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ToId", "IsDelivered");
|
||||||
|
|
||||||
|
b.ToTable("Messages");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MessengerApi.Db.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ApiKey")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FromId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Npg.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Initial : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Messages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
FromId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ToId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
IsDelivered = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
IsAcknowledged = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
PayloadType = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Payload = table.Column<string>(type: "text", nullable: true),
|
||||||
|
PayloadLifespanInSeconds = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Messages", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ApiKey = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: true),
|
||||||
|
IsEnabled = table.Column<bool>(type: "boolean", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserRoutes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
FromId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
ToId = table.Column<Guid>(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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Messages");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserRoutes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,120 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("FromId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAcknowledged")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDelivered")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Payload")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("PayloadLifespanInSeconds")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("PayloadType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("ToId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ToId", "IsDelivered");
|
||||||
|
|
||||||
|
b.ToTable("Messages");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MessengerApi.Db.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ApiKey")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FromId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Sql.Migrator
|
||||||
|
{
|
||||||
|
public partial class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<MessengerSqlDbContext>
|
||||||
|
{
|
||||||
|
public MessengerSqlDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
return new MessengerSqlDbContext(this.ConnectionString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MessengerApi.Db.Sql\MessengerApi.Db.Sql.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
16
code/MessengerApi.Db.Sql.Migrator/Program.cs
Normal file
16
code/MessengerApi.Db.Sql.Migrator/Program.cs
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
code/MessengerApi.Db.Sql/MessengerApi.Db.Sql.csproj
Normal file
21
code/MessengerApi.Db.Sql/MessengerApi.Db.Sql.csproj
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MessengerApi.Db\MessengerApi.Db.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Migrations\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
31
code/MessengerApi.Db.Sql/MessengerSqlDbContext.cs
Normal file
31
code/MessengerApi.Db.Sql/MessengerSqlDbContext.cs
Normal file
@ -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<Message>().HasIndex(e => new { e.ToId, e.IsDelivered }).IsUnique(false).IsClustered(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.Designer.cs
generated
Normal file
125
code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.Designer.cs
generated
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid>("FromId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAcknowledged")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDelivered")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Payload")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("PayloadLifespanInSeconds")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("PayloadType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("ApiKey")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FromId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.cs
Normal file
100
code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.cs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Sql.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Initial : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Messages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
CreatedUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
FromId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
ToId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
IsDelivered = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
IsAcknowledged = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
PayloadType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
Payload = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
PayloadLifespanInSeconds = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Messages", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
ApiKey = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsEnabled = table.Column<bool>(type: "bit", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserRoutes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
FromId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
|
ToId = table.Column<Guid>(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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Messages");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserRoutes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid>("FromId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAcknowledged")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDelivered")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Payload")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("PayloadLifespanInSeconds")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("PayloadType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<Guid>("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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("ApiKey")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FromId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Converters
|
||||||
|
{
|
||||||
|
public sealed class DateTimeAsUtcValueConverter()
|
||||||
|
: ValueConverter<DateTime, DateTime>(
|
||||||
|
v => v, v => new DateTime(v.Ticks, DateTimeKind.Utc));
|
||||||
|
}
|
||||||
19
code/MessengerApi.Db/MessengerApi.Db.csproj
Normal file
19
code/MessengerApi.Db/MessengerApi.Db.csproj
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
<BaseOutputPath>..\out\</BaseOutputPath>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MessengerApi.Db.Contracts\MessengerApi.Db.Contracts.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
26
code/MessengerApi.Db/MessengerDbContext.cs
Normal file
26
code/MessengerApi.Db/MessengerDbContext.cs
Normal file
@ -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<User> Users { get; set; }
|
||||||
|
|
||||||
|
public DbSet<Message> Messages { get; set; }
|
||||||
|
|
||||||
|
public DbSet<UserRoute> UserRoutes { get; set; }
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<User>().HasKey(e => e.Id);
|
||||||
|
modelBuilder.Entity<Message>().HasKey(e => e.Id);
|
||||||
|
modelBuilder.Entity<Message>().Property(e => e.CreatedUtc).HasConversion<DateTimeAsUtcValueConverter>();
|
||||||
|
modelBuilder.Entity<Message>().Property(e => e.PayloadLifespanInSeconds).IsRequired();
|
||||||
|
modelBuilder.Entity<UserRoute>().HasKey(e => e.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
code/MessengerApi.Db/Repositories/MessageRepository.cs
Normal file
23
code/MessengerApi.Db/Repositories/MessageRepository.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using MessengerApi.Db.Contracts.Repositories;
|
||||||
|
using MessengerApi.Db.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Repositories
|
||||||
|
{
|
||||||
|
public class MessageRepository : Repository<Message>, IMessageRepository
|
||||||
|
{
|
||||||
|
public MessageRepository(DbSet<Message> db) : base(db)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Message> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
code/MessengerApi.Db/Repositories/Repository.cs
Normal file
26
code/MessengerApi.Db/Repositories/Repository.cs
Normal file
@ -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<T> : IRepository<T> where T : class, IEntity<T>
|
||||||
|
{
|
||||||
|
protected readonly DbSet<T> db;
|
||||||
|
|
||||||
|
public Repository(DbSet<T> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
code/MessengerApi.Db/Repositories/UserRepository.cs
Normal file
18
code/MessengerApi.Db/Repositories/UserRepository.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using MessengerApi.Db.Contracts.Repositories;
|
||||||
|
using MessengerApi.Db.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Repositories
|
||||||
|
{
|
||||||
|
public class UserRepository : Repository<User>, IUserRepository
|
||||||
|
{
|
||||||
|
public UserRepository(DbSet<User> db) : base(db)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public User SingleByApiKeyAndEnabled(Guid id, bool enabled)
|
||||||
|
{
|
||||||
|
return this.db.Single(x => x.ApiKey == id && x.IsEnabled == enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
code/MessengerApi.Db/Repositories/UserRouteRepository.cs
Normal file
23
code/MessengerApi.Db/Repositories/UserRouteRepository.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using MessengerApi.Db.Contracts.Repositories;
|
||||||
|
using MessengerApi.Db.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace MessengerApi.Db.Repositories
|
||||||
|
{
|
||||||
|
public class UserRouteRepository : Repository<UserRoute>, IUserRouteRepository
|
||||||
|
{
|
||||||
|
public UserRouteRepository(DbSet<UserRoute> db) : base(db)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<UserRoute> 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<UserRoute> GetByFrom(User user)
|
||||||
|
{
|
||||||
|
return this.db.Include(x => x.From).Include(x => x.To).Where(x => x.From.Id == user.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
code/MessengerApi.sln
Normal file
156
code/MessengerApi.sln
Normal file
@ -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
|
||||||
13
code/MessengerApi/.config/dotnet-tools.json
Normal file
13
code/MessengerApi/.config/dotnet-tools.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "8.0.10",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
code/MessengerApi/Constants.cs
Normal file
7
code/MessengerApi/Constants.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace MessengerApi
|
||||||
|
{
|
||||||
|
public class Constants
|
||||||
|
{
|
||||||
|
public const string USERFILE_FILENAME = "/app/users.conf";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
using MessengerApi.Db;
|
||||||
|
|
||||||
|
namespace MessengerApi.Contracts.Factories
|
||||||
|
{
|
||||||
|
public interface IDbContextFactory
|
||||||
|
{
|
||||||
|
MessengerDbContext CreateDbContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
15
code/MessengerApi/Contracts/Models/Scoped/IUnitOfWork.cs
Normal file
15
code/MessengerApi/Contracts/Models/Scoped/IUnitOfWork.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
code/MessengerApi/Dockerfile
Normal file
8
code/MessengerApi/Dockerfile
Normal file
@ -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"]
|
||||||
34
code/MessengerApi/Factories/DbContextFactory.cs
Normal file
34
code/MessengerApi/Factories/DbContextFactory.cs
Normal file
@ -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<MessengerDbContext>
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
code/MessengerApi/Factories/LoggerFactory.cs
Normal file
32
code/MessengerApi/Factories/LoggerFactory.cs
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
code/MessengerApi/GlobalUsings.cs
Normal file
2
code/MessengerApi/GlobalUsings.cs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
global using portaloggy;
|
||||||
|
global using ILogger = portaloggy.ILogger;
|
||||||
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates our permananet API keys sent over as Bearer tokens.
|
||||||
|
/// </summary>
|
||||||
|
public class CustomBearerAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache memoryCache;
|
||||||
|
|
||||||
|
public CustomBearerAuthenticationHandler(
|
||||||
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
UrlEncoder encoder,
|
||||||
|
IMemoryCache memoryCache)
|
||||||
|
: base(options, loggerFactory, encoder)
|
||||||
|
{
|
||||||
|
this.memoryCache = memoryCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
const string HEADER = "Authorization";
|
||||||
|
const string PREFIX = "Bearer ";
|
||||||
|
|
||||||
|
Context.RequestServices.GetRequiredService<Timing>(); // 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>();
|
||||||
|
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<IUnitOfWork>();
|
||||||
|
var user = unitOfWork.Users.SingleByApiKeyAndEnabled(Guid.Parse(token), true);
|
||||||
|
var routes = unitOfWork.UserRoutes.GetAllByUser(user).ToArray();
|
||||||
|
|
||||||
|
var principal = new ClaimsPrincipal(
|
||||||
|
new ClaimsIdentity(
|
||||||
|
new List<Claim>
|
||||||
|
{
|
||||||
|
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>();
|
||||||
|
identity.User = cache.User;
|
||||||
|
identity.UserRoutes = cache.UserRoutes;
|
||||||
|
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(cache.ClaimsPrincipal, Scheme.Name)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
code/MessengerApi/Handlers/Endpoint/AckEndpointHandler.cs
Normal file
40
code/MessengerApi/Handlers/Endpoint/AckEndpointHandler.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
code/MessengerApi/Handlers/Endpoint/PeekEndpointHandler.cs
Normal file
35
code/MessengerApi/Handlers/Endpoint/PeekEndpointHandler.cs
Normal file
@ -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<int> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<Message[]> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
code/MessengerApi/Handlers/Endpoint/SendEndpointHandler.cs
Normal file
60
code/MessengerApi/Handlers/Endpoint/SendEndpointHandler.cs
Normal file
@ -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<Message> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
code/MessengerApi/Handlers/HousekeepingHandler.cs
Normal file
47
code/MessengerApi/Handlers/HousekeepingHandler.cs
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
code/MessengerApi/Handlers/UserSetupHandler.cs
Normal file
99
code/MessengerApi/Handlers/UserSetupHandler.cs
Normal file
@ -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<UserSetupItem[]> ReadLines(string[] lines)
|
||||||
|
{
|
||||||
|
var items = new List<UserSetupItem>();
|
||||||
|
|
||||||
|
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<UserSetupItem> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
code/MessengerApi/MessengerApi.csproj
Normal file
30
code/MessengerApi/MessengerApi.csproj
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UserSecretsId>85c81e87-1274-45ce-8b91-6d6619ffdfa2</UserSecretsId>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<BaseOutputPath>..\out\</BaseOutputPath>
|
||||||
|
<StartupObject>MessengerApi.Api.Program</StartupObject>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" />
|
||||||
|
<PackageReference Include="portaloggy" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MessengerApi.Configuration\MessengerApi.Configuration.csproj" />
|
||||||
|
<ProjectReference Include="..\MessengerApi.Contracts\MessengerApi.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\MessengerApi.Db.Npg\MessengerApi.Db.Npg.csproj" />
|
||||||
|
<ProjectReference Include="..\MessengerApi.Db.Sql\MessengerApi.Db.Sql.csproj" />
|
||||||
|
<ProjectReference Include="..\MessengerApi.Db\MessengerApi.Db.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
13
code/MessengerApi/Models/CachedIdentity.cs
Normal file
13
code/MessengerApi/Models/CachedIdentity.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
7
code/MessengerApi/Models/Http/AckRequest.cs
Normal file
7
code/MessengerApi/Models/Http/AckRequest.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace MessengerApi.Models.Http
|
||||||
|
{
|
||||||
|
public class AckRequest
|
||||||
|
{
|
||||||
|
public Guid MessageId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
code/MessengerApi/Models/Http/SendRequest.cs
Normal file
13
code/MessengerApi/Models/Http/SendRequest.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
7
code/MessengerApi/Models/Http/VerifyRequest.cs
Normal file
7
code/MessengerApi/Models/Http/VerifyRequest.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace MessengerApi.Models.Http
|
||||||
|
{
|
||||||
|
public class VerifyRequest
|
||||||
|
{
|
||||||
|
public Guid MessageId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
code/MessengerApi/Models/Scoped/Identity.cs
Normal file
9
code/MessengerApi/Models/Scoped/Identity.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
7
code/MessengerApi/Models/Scoped/Timing.cs
Normal file
7
code/MessengerApi/Models/Scoped/Timing.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace MessengerApi.Models.Scoped
|
||||||
|
{
|
||||||
|
public class Timing
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
code/MessengerApi/Models/Scoped/UnitOfWork.cs
Normal file
33
code/MessengerApi/Models/Scoped/UnitOfWork.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
code/MessengerApi/Models/UserSetupItem.cs
Normal file
11
code/MessengerApi/Models/UserSetupItem.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
313
code/MessengerApi/Program.cs
Normal file
313
code/MessengerApi/Program.cs
Normal file
@ -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<MessengerConfiguration>(configuration);
|
||||||
|
builder.Services.AddSingleton<ILogger>(new Factories.LoggerFactory(configuration).CreateLogger());
|
||||||
|
builder.Services.AddSingleton<SendEndpointHandler>();
|
||||||
|
builder.Services.AddSingleton<HousekeepingHandler>();
|
||||||
|
builder.Services.AddSingleton<UserSetupHandler>();
|
||||||
|
builder.Services.AddSingleton<IDbContextFactory, DbContextFactory>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<Timing>();
|
||||||
|
builder.Services.AddScoped<Identity>();
|
||||||
|
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||||
|
builder.Services.AddScoped<SendEndpointHandler>();
|
||||||
|
builder.Services.AddScoped<ReceiveEndpointHandler>();
|
||||||
|
builder.Services.AddScoped<AckEndpointHandler>();
|
||||||
|
builder.Services.AddScoped<PeekEndpointHandler>();
|
||||||
|
|
||||||
|
// Authentication.
|
||||||
|
builder.Services
|
||||||
|
.AddAuthentication("Bearer")
|
||||||
|
.AddScheme<AuthenticationSchemeOptions, CustomBearerAuthenticationHandler>("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, string>(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<ForwardedHeadersOptions>(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<IDbContextFactory>().CreateDbContext())
|
||||||
|
{
|
||||||
|
var migrationLogger = app.Services.GetRequiredService<ILogger>();
|
||||||
|
|
||||||
|
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<HousekeepingHandler>().RemoveOldMessages();
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// User synchronization
|
||||||
|
var userSetupHandler = app.Services.GetRequiredService<UserSetupHandler>();
|
||||||
|
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<ILogger>();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
code/MessengerApi/Properties/launchSettings.json
Normal file
28
code/MessengerApi/Properties/launchSettings.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
messengerapi:
|
||||||
|
image: https://gitea.masita.net/mc/messengerapi:latest
|
||||||
|
container_name: messengerapi
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
Reference in New Issue
Block a user