Compare commits
97 Commits
8c748fd57a
...
refactor/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
f6f0703cb3
|
|||
|
3d47b4e44e
|
|||
|
71fe2a30e7
|
|||
|
d8f57161ae
|
|||
|
3caa79b9a7
|
|||
|
49beb17925
|
|||
|
bd8e13f25d
|
|||
|
1128c9a0ba
|
|||
|
8dfe201afe
|
|||
|
c1016e496a
|
|||
|
091097a858
|
|||
|
5c97733b3e
|
|||
|
4ee387ab76
|
|||
|
19bf17200d
|
|||
|
be6d97ec85
|
|||
|
9d282b26f3
|
|||
|
dbc2c54ab0
|
|||
|
aa062932cf
|
|||
|
812dd03e85
|
|||
|
06d639a114
|
|||
|
74f51036b1
|
|||
|
8308325b73
|
|||
|
fa7010db3d
|
|||
|
89320fc540
|
|||
|
5ec8d89563
|
|||
|
0eeafb5352
|
|||
|
ab2bdcc7ca
|
|||
|
c2b49e6642
|
|||
|
1a89c48790
|
|||
|
8dddfe77cd
|
|||
|
8e8b011fdd
|
|||
|
abd346bb97
|
|||
|
6386ec8caa
|
|||
|
ad062828ff
|
|||
|
92e4988114
|
|||
|
f9269d7558
|
|||
|
fa01b7027a
|
|||
|
eaa3a9c297
|
|||
|
6cedda9307
|
|||
|
942ca73f8d
|
|||
|
da3f58f2ec
|
|||
|
4a8521d59d
|
|||
|
d7ad84e199
|
|||
|
52430c19a5
|
|||
|
9492b6cac6
|
|||
|
5f324a2348
|
|||
|
7452b14817
|
|||
|
4a27794ccc
|
|||
|
d2f5ba36ab
|
|||
| 0117fdf084 | |||
| 02680d224a | |||
| 68bfdebcbd | |||
| 54907eede1 | |||
| a21d19c3ef | |||
| df732616d5 | |||
| 79a31ae060 | |||
| 6eacfcd8f2 | |||
| 5e328509bd | |||
| 9c078db564 | |||
| ddd109c77c | |||
| 3ee04d0b24 | |||
| 7f110313e9 | |||
| bc2e87c56f | |||
| d7271a2d11 | |||
| c57d65db67 | |||
| edf3aab173 | |||
| 352746a141 | |||
| 216c72ea36 | |||
| d0723b366b | |||
| fb6721cb1b | |||
| 9fcb169c94 | |||
| 572874431d | |||
| f595ac8001 | |||
| 18674e0e1d | |||
| da4c4d3a84 | |||
| aec01b117d | |||
| d299c32e35 | |||
| 344007af66 | |||
| d4de5aeac2 | |||
| 8ce5ba50f4 | |||
| 5a44952b27 | |||
| c30946daf6 | |||
| 0221d7b294 | |||
| c44b0b64c3 | |||
| 442ee3bcfd | |||
| 081815c512 | |||
| eab2a388ae | |||
| 5f7ab49abb | |||
| 4ff89173b2 | |||
| f2052410c7 | |||
| 83a49be725 | |||
| 9b205a73fd | |||
| d5157eb7e3 | |||
| 75c92c51db | |||
| 915054fce0 | |||
| 63653680ba | |||
| 84c4df6620 |
3
.aspire/settings.json
Normal file
3
.aspire/settings.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"appHostPath": "../DysonNetwork.Control/DysonNetwork.Control.csproj"
|
||||||
|
}
|
||||||
35
.env
Normal file
35
.env
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Default container port for ring
|
||||||
|
RING_PORT=8080
|
||||||
|
|
||||||
|
# Default container port for pass
|
||||||
|
PASS_PORT=8080
|
||||||
|
|
||||||
|
# Default container port for drive
|
||||||
|
DRIVE_PORT=8080
|
||||||
|
|
||||||
|
# Default container port for sphere
|
||||||
|
SPHERE_PORT=8080
|
||||||
|
|
||||||
|
# Default container port for develop
|
||||||
|
DEVELOP_PORT=8080
|
||||||
|
|
||||||
|
# Parameter cache-password
|
||||||
|
CACHE_PASSWORD=KS3jSPaU9e
|
||||||
|
|
||||||
|
# Parameter queue-password
|
||||||
|
QUEUE_PASSWORD=8xEECa4ckz
|
||||||
|
|
||||||
|
# Container image name for ring
|
||||||
|
RING_IMAGE=ring:latest
|
||||||
|
|
||||||
|
# Container image name for pass
|
||||||
|
PASS_IMAGE=pass:latest
|
||||||
|
|
||||||
|
# Container image name for drive
|
||||||
|
DRIVE_IMAGE=drive:latest
|
||||||
|
|
||||||
|
# Container image name for sphere
|
||||||
|
SPHERE_IMAGE=sphere:latest
|
||||||
|
|
||||||
|
# Container image name for develop
|
||||||
|
DEVELOP_IMAGE=develop:latest
|
||||||
249
.github/workflows/docker-build.yml
vendored
249
.github/workflows/docker-build.yml
vendored
@@ -1,189 +1,60 @@
|
|||||||
name: Build and Push Microservices
|
name: Aspire Publish Workflow
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-sphere:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
- name: Setup .NET
|
||||||
- name: Setup NBGV
|
uses: actions/setup-dotnet@v3
|
||||||
uses: dotnet/nbgv@master
|
with:
|
||||||
id: nbgv
|
dotnet-version: "9.0.x"
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
- name: Log in to GitHub Container Registry
|
||||||
- name: Log in to GitHub Container Registry
|
uses: docker/login-action@v3
|
||||||
uses: docker/login-action@v3
|
with:
|
||||||
with:
|
registry: ghcr.io
|
||||||
registry: ghcr.io
|
username: ${{ github.actor }}
|
||||||
username: ${{ github.actor }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build and push DysonNetwork.Sphere Docker image
|
- name: Install Aspire CLI
|
||||||
uses: docker/build-push-action@v6
|
run: dotnet tool install -g Aspire.Cli --prerelease
|
||||||
with:
|
|
||||||
file: DysonNetwork.Sphere/Dockerfile
|
- name: Build and Publish Aspire Application
|
||||||
context: .
|
run: aspire publish --project ./DysonNetwork.Control/DysonNetwork.Control.csproj --output publish
|
||||||
push: true
|
|
||||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-sphere:latest
|
- name: Tag and Push Images
|
||||||
platforms: linux/amd64
|
run: |
|
||||||
|
IMAGES=( "sphere" "pass" "ring" "drive" "develop" )
|
||||||
build-pass:
|
|
||||||
runs-on: ubuntu-latest
|
for image in "${IMAGES[@]}"; do
|
||||||
permissions:
|
IMAGE_NAME="ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-$image:alpha"
|
||||||
contents: read
|
SOURCE_IMAGE_NAME="$image:latest" # Aspire's default local image name
|
||||||
packages: write
|
|
||||||
steps:
|
echo "Tagging and pushing $SOURCE_IMAGE_NAME to $IMAGE_NAME..."
|
||||||
- name: Checkout repository
|
docker tag $SOURCE_IMAGE_NAME $IMAGE_NAME
|
||||||
uses: actions/checkout@v3
|
docker push $IMAGE_NAME
|
||||||
with:
|
done
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup NBGV
|
- name: Upload Aspire Publish Directory
|
||||||
uses: dotnet/nbgv@master
|
uses: actions/upload-artifact@v3
|
||||||
id: nbgv
|
with:
|
||||||
- name: Set up Docker Buildx
|
name: aspire-publish-output
|
||||||
uses: docker/setup-buildx-action@v3
|
path: ./publish/
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
- name: Upload Docker Compose file
|
||||||
with:
|
uses: actions/upload-artifact@v3
|
||||||
registry: ghcr.io
|
with:
|
||||||
username: ${{ github.actor }}
|
name: docker-compose-output
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
path: ./publish/docker-compose.yml
|
||||||
- name: Build and push DysonNetwork.Pass Docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
file: DysonNetwork.Pass/Dockerfile
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pass:latest
|
|
||||||
platforms: linux/amd64
|
|
||||||
|
|
||||||
build-pusher:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup NBGV
|
|
||||||
uses: dotnet/nbgv@master
|
|
||||||
id: nbgv
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build and push DysonNetwork.Pusher Docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
file: DysonNetwork.Pusher/Dockerfile
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pusher:latest
|
|
||||||
platforms: linux/amd64
|
|
||||||
|
|
||||||
build-drive:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup NBGV
|
|
||||||
uses: dotnet/nbgv@master
|
|
||||||
id: nbgv
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build and push DysonNetwork.Drive Docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
file: DysonNetwork.Drive/Dockerfile
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-drive:latest
|
|
||||||
platforms: linux/amd64
|
|
||||||
|
|
||||||
build-gateway:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup NBGV
|
|
||||||
uses: dotnet/nbgv@master
|
|
||||||
id: nbgv
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build and push DysonNetwork.Gateway Docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
file: DysonNetwork.Gateway/Dockerfile
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-gateway:latest
|
|
||||||
platforms: linux/amd64
|
|
||||||
|
|
||||||
build-develop:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup NBGV
|
|
||||||
uses: dotnet/nbgv@master
|
|
||||||
id: nbgv
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build and push DysonNetwork.Develop Docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
file: DysonNetwork.Develop/Dockerfile
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-develop:latest
|
|
||||||
platforms: linux/amd64
|
|
||||||
|
|
||||||
|
|||||||
77
DysonNetwork.Control/AppHost.cs
Normal file
77
DysonNetwork.Control/AppHost.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
using Aspire.Hosting.Yarp.Transforms;
|
||||||
|
|
||||||
|
var builder = DistributedApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Database was configured separately in each service.
|
||||||
|
// var database = builder.AddPostgres("database");
|
||||||
|
|
||||||
|
var cache = builder.AddRedis("cache");
|
||||||
|
var queue = builder.AddNats("queue").WithJetStream();
|
||||||
|
|
||||||
|
var ringService = builder.AddProject<Projects.DysonNetwork_Ring>("ring")
|
||||||
|
.WithReference(queue)
|
||||||
|
.WithHttpHealthCheck()
|
||||||
|
.WithEndpoint(5001, 5001, "https", name: "grpc");
|
||||||
|
var passService = builder.AddProject<Projects.DysonNetwork_Pass>("pass")
|
||||||
|
.WithReference(cache)
|
||||||
|
.WithReference(queue)
|
||||||
|
.WithReference(ringService)
|
||||||
|
.WithHttpHealthCheck()
|
||||||
|
.WithEndpoint(5001, 5001, "https", name: "grpc");
|
||||||
|
var driveService = builder.AddProject<Projects.DysonNetwork_Drive>("drive")
|
||||||
|
.WithReference(cache)
|
||||||
|
.WithReference(queue)
|
||||||
|
.WithReference(passService)
|
||||||
|
.WithReference(ringService)
|
||||||
|
.WithHttpHealthCheck()
|
||||||
|
.WithEndpoint(5001, 5001, "https", name: "grpc");
|
||||||
|
var sphereService = builder.AddProject<Projects.DysonNetwork_Sphere>("sphere")
|
||||||
|
.WithReference(cache)
|
||||||
|
.WithReference(queue)
|
||||||
|
.WithReference(passService)
|
||||||
|
.WithReference(ringService)
|
||||||
|
.WithHttpHealthCheck()
|
||||||
|
.WithEndpoint(5001, 5001, "https", name: "grpc");
|
||||||
|
var developService = builder.AddProject<Projects.DysonNetwork_Develop>("develop")
|
||||||
|
.WithReference(cache)
|
||||||
|
.WithReference(passService)
|
||||||
|
.WithReference(ringService)
|
||||||
|
.WithHttpHealthCheck()
|
||||||
|
.WithEndpoint(5001, 5001, "https", name: "grpc");
|
||||||
|
|
||||||
|
// Extra double-ended references
|
||||||
|
ringService.WithReference(passService);
|
||||||
|
|
||||||
|
builder.AddYarp("gateway")
|
||||||
|
.WithHostPort(5000)
|
||||||
|
.WithConfiguration(yarp =>
|
||||||
|
{
|
||||||
|
var ringCluster = yarp.AddCluster(ringService.GetEndpoint("http"));
|
||||||
|
yarp.AddRoute("/ws", ringCluster);
|
||||||
|
yarp.AddRoute("/ring/{**catch-all}", ringCluster)
|
||||||
|
.WithTransformPathRemovePrefix("/ring")
|
||||||
|
.WithTransformPathPrefix("/api");
|
||||||
|
var passCluster = yarp.AddCluster(passService.GetEndpoint("http"));
|
||||||
|
yarp.AddRoute("/.well-known/openid-configuration", passCluster);
|
||||||
|
yarp.AddRoute("/.well-known/jwks", passCluster);
|
||||||
|
yarp.AddRoute("/id/{**catch-all}", passCluster)
|
||||||
|
.WithTransformPathRemovePrefix("/id")
|
||||||
|
.WithTransformPathPrefix("/api");
|
||||||
|
var driveCluster = yarp.AddCluster(driveService.GetEndpoint("http"));
|
||||||
|
yarp.AddRoute("/api/tus", driveCluster);
|
||||||
|
yarp.AddRoute("/drive/{**catch-all}", driveCluster)
|
||||||
|
.WithTransformPathRemovePrefix("/drive")
|
||||||
|
.WithTransformPathPrefix("/api");
|
||||||
|
var sphereCluster = yarp.AddCluster(sphereService.GetEndpoint("http"));
|
||||||
|
yarp.AddRoute("/sphere/{**catch-all}", sphereCluster)
|
||||||
|
.WithTransformPathRemovePrefix("/sphere")
|
||||||
|
.WithTransformPathPrefix("/api");
|
||||||
|
var developCluster = yarp.AddCluster(developService.GetEndpoint("http"));
|
||||||
|
yarp.AddRoute("/develop/{**catch-all}", developCluster)
|
||||||
|
.WithTransformPathRemovePrefix("/develop")
|
||||||
|
.WithTransformPathPrefix("/api");
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.AddDockerComposeEnvironment("docker-compose");
|
||||||
|
|
||||||
|
builder.Build().Run();
|
||||||
30
DysonNetwork.Control/DysonNetwork.Control.csproj
Normal file
30
DysonNetwork.Control/DysonNetwork.Control.csproj
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Sdk Name="Aspire.AppHost.Sdk" Version="9.4.2"/>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<UserSecretsId>a68b3195-a00d-40c2-b5ed-d675356b7cde</UserSecretsId>
|
||||||
|
<RootNamespace>DysonNetwork.Control</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.4.2"/>
|
||||||
|
<PackageReference Include="Aspire.Hosting.Docker" Version="9.4.2-preview.1.25428.12" />
|
||||||
|
<PackageReference Include="Aspire.Hosting.Nats" Version="9.4.2" />
|
||||||
|
<PackageReference Include="Aspire.Hosting.Redis" Version="9.4.2" />
|
||||||
|
<PackageReference Include="Aspire.Hosting.Yarp" Version="9.4.2-preview.1.25428.12" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DysonNetwork.Develop\DysonNetwork.Develop.csproj" />
|
||||||
|
<ProjectReference Include="..\DysonNetwork.Drive\DysonNetwork.Drive.csproj" />
|
||||||
|
<ProjectReference Include="..\DysonNetwork.Pass\DysonNetwork.Pass.csproj" />
|
||||||
|
<ProjectReference Include="..\DysonNetwork.Ring\DysonNetwork.Ring.csproj" />
|
||||||
|
<ProjectReference Include="..\DysonNetwork.Sphere\DysonNetwork.Sphere.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
29
DysonNetwork.Control/Properties/launchSettings.json
Normal file
29
DysonNetwork.Control/Properties/launchSettings.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "https://localhost:17025;http://localhost:15057",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"DOTNET_ENVIRONMENT": "Development",
|
||||||
|
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21175",
|
||||||
|
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22189"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:15057",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"DOTNET_ENVIRONMENT": "Development",
|
||||||
|
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163",
|
||||||
|
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20185"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
DysonNetwork.Control/appsettings.json
Normal file
11
DysonNetwork.Control/appsettings.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"cache": "localhost:6379"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" />
|
||||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
using DysonNetwork.Develop;
|
using DysonNetwork.Develop;
|
||||||
using DysonNetwork.Shared.Auth;
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
using DysonNetwork.Shared.Registry;
|
|
||||||
using DysonNetwork.Develop.Startup;
|
using DysonNetwork.Develop.Startup;
|
||||||
using DysonNetwork.Shared.Stream;
|
using DysonNetwork.Shared.Registry;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.AddServiceDefaults();
|
||||||
|
|
||||||
builder.ConfigureAppKestrel(builder.Configuration);
|
builder.ConfigureAppKestrel(builder.Configuration);
|
||||||
|
|
||||||
builder.Services.AddRegistryService(builder.Configuration);
|
|
||||||
builder.Services.AddStreamConnection(builder.Configuration);
|
|
||||||
builder.Services.AddAppServices(builder.Configuration);
|
builder.Services.AddAppServices(builder.Configuration);
|
||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
builder.Services.AddAppSwagger();
|
builder.Services.AddAppSwagger();
|
||||||
@@ -22,6 +21,8 @@ builder.Services.AddDriveService();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.MapDefaultEndpoints();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using DysonNetwork.Develop.Identity;
|
using DysonNetwork.Develop.Identity;
|
||||||
using DysonNetwork.Shared.Auth;
|
using DysonNetwork.Shared.Auth;
|
||||||
|
using DysonNetwork.Shared.Http;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Prometheus;
|
using Prometheus;
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ public static class ApplicationConfiguration
|
|||||||
|
|
||||||
app.UseRequestLocalization();
|
app.UseRequestLocalization();
|
||||||
|
|
||||||
ConfigureForwardedHeaders(app, configuration);
|
app.ConfigureForwardedHeaders(configuration);
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
@@ -30,26 +31,4 @@ public static class ApplicationConfiguration
|
|||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureForwardedHeaders(WebApplication app, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
var knownProxiesSection = configuration.GetSection("KnownProxies");
|
|
||||||
var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
|
|
||||||
|
|
||||||
if (knownProxiesSection.Exists())
|
|
||||||
{
|
|
||||||
var proxyAddresses = knownProxiesSection.Get<string[]>();
|
|
||||||
if (proxyAddresses != null)
|
|
||||||
foreach (var proxy in proxyAddresses)
|
|
||||||
if (IPAddress.TryParse(proxy, out var ipAddress))
|
|
||||||
forwardedHeadersOptions.KnownProxies.Add(ipAddress);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.Any);
|
|
||||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.IPv6Any);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseForwardedHeaders(forwardedHeadersOptions);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Microsoft.OpenApi.Models;
|
|||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.SystemTextJson;
|
using NodaTime.Serialization.SystemTextJson;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Develop.Identity;
|
using DysonNetwork.Develop.Identity;
|
||||||
using DysonNetwork.Develop.Project;
|
using DysonNetwork.Develop.Project;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
@@ -19,19 +20,16 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddDbContext<AppDatabase>();
|
services.AddDbContext<AppDatabase>();
|
||||||
services.AddSingleton<IClock>(SystemClock.Instance);
|
services.AddSingleton<IClock>(SystemClock.Instance);
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
services.AddSingleton<IConnectionMultiplexer>(_ =>
|
|
||||||
{
|
|
||||||
var connection = configuration.GetConnectionString("FastRetrieve")!;
|
|
||||||
return ConnectionMultiplexer.Connect(connection);
|
|
||||||
});
|
|
||||||
services.AddSingleton<ICacheService, CacheServiceRedis>();
|
services.AddSingleton<ICacheService, CacheServiceRedis>();
|
||||||
|
|
||||||
services.AddHttpClient();
|
services.AddHttpClient();
|
||||||
|
|
||||||
services.AddControllers().AddJsonOptions(options =>
|
services.AddControllers().AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
|
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
||||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||||
|
|
||||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,7 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
|
"App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
|
||||||
"FastRetrieve": "localhost:6379",
|
|
||||||
"Etcd": "etcd.orb.local:2379",
|
|
||||||
"Stream": "nats.orb.local:4222"
|
|
||||||
},
|
},
|
||||||
"KnownProxies": [
|
"KnownProxies": [
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
@@ -24,8 +21,6 @@
|
|||||||
},
|
},
|
||||||
"Service": {
|
"Service": {
|
||||||
"Name": "DysonNetwork.Develop",
|
"Name": "DysonNetwork.Develop",
|
||||||
"Url": "https://localhost:7192",
|
"Url": "https://localhost:7192"
|
||||||
"ClientCert": "../Certificates/client.crt",
|
|
||||||
"ClientKey": "../Certificates/client.key"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ public class AppDatabase(
|
|||||||
opt => opt
|
opt => opt
|
||||||
.ConfigureDataSource(optSource => optSource.EnableDynamicJson())
|
.ConfigureDataSource(optSource => optSource.EnableDynamicJson())
|
||||||
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
|
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
|
||||||
.UseNetTopologySuite()
|
|
||||||
.UseNodaTime()
|
.UseNodaTime()
|
||||||
).UseSnakeCaseNamingConvention();
|
).UseSnakeCaseNamingConvention();
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||||
@@ -67,6 +66,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" />
|
||||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
403
DysonNetwork.Drive/Migrations/20250907070034_RemoveNetTopo.Designer.cs
generated
Normal file
403
DysonNetwork.Drive/Migrations/20250907070034_RemoveNetTopo.Designer.cs
generated
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using DysonNetwork.Drive;
|
||||||
|
using DysonNetwork.Drive.Storage;
|
||||||
|
using DysonNetwork.Shared.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NodaTime;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Drive.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDatabase))]
|
||||||
|
[Migration("20250907070034_RemoveNetTopo")]
|
||||||
|
partial class RemoveNetTopo
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<Instant?>("ExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expired_at");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<long>("Quota")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("quota");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_quota_records");
|
||||||
|
|
||||||
|
b.ToTable("quota_records", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Guid?>("BundleId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("bundle_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<Instant?>("ExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expired_at");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("FileMeta")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("file_meta");
|
||||||
|
|
||||||
|
b.Property<bool>("HasCompression")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("has_compression");
|
||||||
|
|
||||||
|
b.Property<bool>("HasThumbnail")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("has_thumbnail");
|
||||||
|
|
||||||
|
b.Property<string>("Hash")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("hash");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEncrypted")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_encrypted");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMarkedRecycle")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_marked_recycle");
|
||||||
|
|
||||||
|
b.Property<string>("MimeType")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("mime_type");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<Guid?>("PoolId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("pool_id");
|
||||||
|
|
||||||
|
b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("sensitive_marks");
|
||||||
|
|
||||||
|
b.Property<long>("Size")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("size");
|
||||||
|
|
||||||
|
b.Property<string>("StorageId")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("storage_id");
|
||||||
|
|
||||||
|
b.Property<string>("StorageUrl")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("storage_url");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("UploadedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("uploaded_at");
|
||||||
|
|
||||||
|
b.Property<Dictionary<string, object>>("UserMeta")
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("user_meta");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_files");
|
||||||
|
|
||||||
|
b.HasIndex("BundleId")
|
||||||
|
.HasDatabaseName("ix_files_bundle_id");
|
||||||
|
|
||||||
|
b.HasIndex("PoolId")
|
||||||
|
.HasDatabaseName("ix_files_pool_id");
|
||||||
|
|
||||||
|
b.ToTable("files", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("ExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expired_at");
|
||||||
|
|
||||||
|
b.Property<string>("FileId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("file_id");
|
||||||
|
|
||||||
|
b.Property<string>("ResourceId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("resource_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.Property<string>("Usage")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("usage");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_file_references");
|
||||||
|
|
||||||
|
b.HasIndex("FileId")
|
||||||
|
.HasDatabaseName("ix_file_references_file_id");
|
||||||
|
|
||||||
|
b.ToTable("file_references", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(8192)
|
||||||
|
.HasColumnType("character varying(8192)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<Instant?>("ExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expired_at");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<string>("Passcode")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("passcode");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("slug");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_bundles");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_bundles_slug");
|
||||||
|
|
||||||
|
b.ToTable("bundles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid?>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<BillingConfig>("BillingConfig")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("billing_config");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(8192)
|
||||||
|
.HasColumnType("character varying(8192)")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<bool>("IsHidden")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_hidden");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<PolicyConfig>("PolicyConfig")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("policy_config");
|
||||||
|
|
||||||
|
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("storage_config");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_pools");
|
||||||
|
|
||||||
|
b.ToTable("pools", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Drive.Storage.FileBundle", "Bundle")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("BundleId")
|
||||||
|
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||||
|
|
||||||
|
b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("PoolId")
|
||||||
|
.HasConstraintName("fk_files_pools_pool_id");
|
||||||
|
|
||||||
|
b.Navigation("Bundle");
|
||||||
|
|
||||||
|
b.Navigation("Pool");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
|
||||||
|
.WithMany("References")
|
||||||
|
.HasForeignKey("FileId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_file_references_files_file_id");
|
||||||
|
|
||||||
|
b.Navigation("File");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("References");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Files");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Drive.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RemoveNetTopo : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.OldAnnotation("Npgsql:PostgresExtension:postgis", ",,");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@ namespace DysonNetwork.Drive.Migrations
|
|||||||
.HasAnnotation("ProductVersion", "9.0.7")
|
.HasAnnotation("ProductVersion", "9.0.7")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||||
|
|||||||
@@ -5,18 +5,18 @@ using DysonNetwork.Shared.Auth;
|
|||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
using DysonNetwork.Shared.PageData;
|
using DysonNetwork.Shared.PageData;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using DysonNetwork.Shared.Stream;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using tusdotnet.Stores;
|
using tusdotnet.Stores;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.AddServiceDefaults();
|
||||||
|
|
||||||
// Configure Kestrel and server options
|
// Configure Kestrel and server options
|
||||||
builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue);
|
builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue);
|
||||||
|
|
||||||
// Add application services
|
// Add application services
|
||||||
builder.Services.AddRegistryService(builder.Configuration);
|
|
||||||
builder.Services.AddStreamConnection(builder.Configuration);
|
|
||||||
builder.Services.AddAppServices(builder.Configuration);
|
builder.Services.AddAppServices(builder.Configuration);
|
||||||
builder.Services.AddAppRateLimiting();
|
builder.Services.AddAppRateLimiting();
|
||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
@@ -39,6 +39,8 @@ builder.Services.AddTransient<IPageDataProvider, VersionPageData>();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.MapDefaultEndpoints();
|
||||||
|
|
||||||
// Run database migrations
|
// Run database migrations
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
@@ -51,8 +53,6 @@ var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>();
|
|||||||
// Configure application middleware pipeline
|
// Configure application middleware pipeline
|
||||||
app.ConfigureAppMiddleware(tusDiskStore, builder.Environment.ContentRootPath);
|
app.ConfigureAppMiddleware(tusDiskStore, builder.Environment.ContentRootPath);
|
||||||
|
|
||||||
app.MapGatewayProxy();
|
|
||||||
|
|
||||||
app.MapPages(Path.Combine(app.Environment.WebRootPath, "dist", "index.html"));
|
app.MapPages(Path.Combine(app.Environment.WebRootPath, "dist", "index.html"));
|
||||||
|
|
||||||
// Configure gRPC
|
// Configure gRPC
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ using DysonNetwork.Drive.Storage;
|
|||||||
using DysonNetwork.Shared.Stream;
|
using DysonNetwork.Shared.Stream;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NATS.Client.Core;
|
using NATS.Client.Core;
|
||||||
|
using NATS.Client.JetStream.Models;
|
||||||
|
using NATS.Net;
|
||||||
|
|
||||||
namespace DysonNetwork.Drive.Startup;
|
namespace DysonNetwork.Drive.Startup;
|
||||||
|
|
||||||
@@ -14,12 +16,23 @@ public class BroadcastEventHandler(
|
|||||||
{
|
{
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
await foreach (var msg in nats.SubscribeAsync<byte[]>("accounts.deleted", cancellationToken: stoppingToken))
|
var js = nats.CreateJetStreamContext();
|
||||||
|
|
||||||
|
await js.EnsureStreamCreated("account_events", [AccountDeletedEvent.Type]);
|
||||||
|
|
||||||
|
var consumer = await js.CreateOrUpdateConsumerAsync("account_events",
|
||||||
|
new ConsumerConfig("drive_account_deleted_handler"), cancellationToken: stoppingToken);
|
||||||
|
|
||||||
|
await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var evt = JsonSerializer.Deserialize<AccountDeletedEvent>(msg.Data);
|
var evt = JsonSerializer.Deserialize<AccountDeletedEvent>(msg.Data);
|
||||||
if (evt == null) continue;
|
if (evt == null)
|
||||||
|
{
|
||||||
|
await msg.AckAsync(cancellationToken: stoppingToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogInformation("Account deleted: {AccountId}", evt.AccountId);
|
logger.LogInformation("Account deleted: {AccountId}", evt.AccountId);
|
||||||
|
|
||||||
@@ -46,10 +59,13 @@ public class BroadcastEventHandler(
|
|||||||
await transaction.RollbackAsync(cancellationToken: stoppingToken);
|
await transaction.RollbackAsync(cancellationToken: stoppingToken);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await msg.AckAsync(cancellationToken: stoppingToken);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error processing AccountDeleted");
|
logger.LogError(ex, "Error processing AccountDeleted");
|
||||||
|
await msg.NakAsync(cancellationToken: stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.RateLimiting;
|
using System.Threading.RateLimiting;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
@@ -16,11 +17,6 @@ public static class ServiceCollectionExtensions
|
|||||||
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddDbContext<AppDatabase>(); // Assuming you'll have an AppDatabase
|
services.AddDbContext<AppDatabase>(); // Assuming you'll have an AppDatabase
|
||||||
services.AddSingleton<IConnectionMultiplexer>(_ =>
|
|
||||||
{
|
|
||||||
var connection = configuration.GetConnectionString("FastRetrieve")!;
|
|
||||||
return ConnectionMultiplexer.Connect(connection);
|
|
||||||
});
|
|
||||||
services.AddSingleton<IClock>(SystemClock.Instance);
|
services.AddSingleton<IClock>(SystemClock.Instance);
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
services.AddSingleton<ICacheService, CacheServiceRedis>(); // Uncomment if you have CacheServiceRedis
|
services.AddSingleton<ICacheService, CacheServiceRedis>(); // Uncomment if you have CacheServiceRedis
|
||||||
@@ -40,6 +36,7 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
services.AddControllers().AddJsonOptions(options =>
|
services.AddControllers().AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
|
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
||||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||||
|
|
||||||
|
|||||||
@@ -337,8 +337,14 @@ public class FileService(
|
|||||||
if (!pool.PolicyConfig.NoOptimization)
|
if (!pool.PolicyConfig.NoOptimization)
|
||||||
switch (contentType.Split('/')[0])
|
switch (contentType.Split('/')[0])
|
||||||
{
|
{
|
||||||
case "image" when !AnimatedImageTypes.Contains(contentType) &&
|
case "image":
|
||||||
!AnimatedImageExtensions.Contains(fileExtension):
|
if (AnimatedImageTypes.Contains(contentType) || AnimatedImageExtensions.Contains(fileExtension))
|
||||||
|
{
|
||||||
|
logger.LogInformation("Skip optimize file {FileId} due to it is animated...", fileId);
|
||||||
|
uploads.Add((originalFilePath, string.Empty, contentType, false));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
newMimeType = "image/webp";
|
newMimeType = "image/webp";
|
||||||
using (var vipsImage = Image.NewFromFile(originalFilePath))
|
using (var vipsImage = Image.NewFromFile(originalFilePath))
|
||||||
{
|
{
|
||||||
@@ -672,8 +678,8 @@ public class FileService(
|
|||||||
foreach (var file in fileGroup)
|
foreach (var file in fileGroup)
|
||||||
{
|
{
|
||||||
objectsToDelete.Add(file.StorageId ?? file.Id);
|
objectsToDelete.Add(file.StorageId ?? file.Id);
|
||||||
if(file.HasCompression) objectsToDelete.Add(file.StorageId ?? file.Id + ".compressed");
|
if (file.HasCompression) objectsToDelete.Add(file.StorageId ?? file.Id + ".compressed");
|
||||||
if(file.HasThumbnail) objectsToDelete.Add(file.StorageId ?? file.Id + ".thumbnail");
|
if (file.HasThumbnail) objectsToDelete.Add(file.StorageId ?? file.Id + ".thumbnail");
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.RemoveObjectsAsync(
|
await client.RemoveObjectsAsync(
|
||||||
|
|||||||
@@ -10,10 +10,7 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"App": "Host=localhost;Port=5432;Database=dyson_drive;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
|
"App": "Host=localhost;Port=5432;Database=dyson_drive;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
|
||||||
"FastRetrieve": "localhost:6379",
|
|
||||||
"Etcd": "etcd.orb.local:2379",
|
|
||||||
"Stream": "nats.orb.local:4222"
|
|
||||||
},
|
},
|
||||||
"Authentication": {
|
"Authentication": {
|
||||||
"Schemes": {
|
"Schemes": {
|
||||||
@@ -131,8 +128,6 @@
|
|||||||
],
|
],
|
||||||
"Service": {
|
"Service": {
|
||||||
"Name": "DysonNetwork.Drive",
|
"Name": "DysonNetwork.Drive",
|
||||||
"Url": "https://localhost:7092",
|
"Url": "https://localhost:7092"
|
||||||
"ClientCert": "../Certificates/client.crt",
|
|
||||||
"ClientKey": "../Certificates/client.key"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using dotnet_etcd.interfaces;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Yarp.ReverseProxy.Configuration;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Gateway.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("/.well-known")]
|
|
||||||
public class WellKnownController(
|
|
||||||
IConfiguration configuration,
|
|
||||||
IProxyConfigProvider proxyConfigProvider,
|
|
||||||
IEtcdClient etcdClient)
|
|
||||||
: ControllerBase
|
|
||||||
{
|
|
||||||
[HttpGet("domains")]
|
|
||||||
public IActionResult GetDomainMappings()
|
|
||||||
{
|
|
||||||
var domainMappings = configuration.GetSection("DomainMappings").GetChildren()
|
|
||||||
.ToDictionary(x => x.Key, x => x.Value);
|
|
||||||
return Ok(domainMappings);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("services")]
|
|
||||||
public IActionResult GetServices()
|
|
||||||
{
|
|
||||||
var local = configuration.GetValue<bool>("LocalMode");
|
|
||||||
var response = etcdClient.GetRange("/services/");
|
|
||||||
var kvs = response.Kvs;
|
|
||||||
|
|
||||||
var serviceMap = kvs.ToDictionary(
|
|
||||||
kv => Encoding.UTF8.GetString(kv.Key.ToByteArray()).Replace("/services/", ""),
|
|
||||||
kv => Encoding.UTF8.GetString(kv.Value.ToByteArray())
|
|
||||||
);
|
|
||||||
|
|
||||||
if (local) return Ok(serviceMap);
|
|
||||||
|
|
||||||
var domainMappings = configuration.GetSection("DomainMappings").GetChildren()
|
|
||||||
.ToDictionary(x => x.Key, x => x.Value);
|
|
||||||
foreach (var (key, _) in serviceMap.ToList())
|
|
||||||
{
|
|
||||||
if (!domainMappings.TryGetValue(key, out var domain)) continue;
|
|
||||||
if (domain is not null)
|
|
||||||
serviceMap[key] = "https://" + domain;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(serviceMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("routes")]
|
|
||||||
public IActionResult GetProxyRules()
|
|
||||||
{
|
|
||||||
var config = proxyConfigProvider.GetConfig();
|
|
||||||
var rules = config.Routes.Select(r => new
|
|
||||||
{
|
|
||||||
r.RouteId,
|
|
||||||
r.ClusterId,
|
|
||||||
Match = new
|
|
||||||
{
|
|
||||||
r.Match.Path,
|
|
||||||
Hosts = r.Match.Hosts != null ? string.Join(", ", r.Match.Hosts) : null
|
|
||||||
},
|
|
||||||
Transforms = r.Transforms?.Select(t => t.Select(kv => $"{kv.Key}: {kv.Value}").ToList())
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
var clusters = config.Clusters.Select(c => new
|
|
||||||
{
|
|
||||||
c.ClusterId,
|
|
||||||
Destinations = c.Destinations?.Select(d => new
|
|
||||||
{
|
|
||||||
d.Key,
|
|
||||||
d.Value.Address
|
|
||||||
}).ToList()
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
return Ok(new { Rules = rules, Clusters = clusters });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
|
||||||
USER $APP_UID
|
|
||||||
WORKDIR /app
|
|
||||||
EXPOSE 8080
|
|
||||||
EXPOSE 8081
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
WORKDIR /src
|
|
||||||
COPY ["DysonNetwork.Gateway/DysonNetwork.Gateway.csproj", "DysonNetwork.Gateway/"]
|
|
||||||
RUN dotnet restore "DysonNetwork.Gateway/DysonNetwork.Gateway.csproj"
|
|
||||||
COPY . .
|
|
||||||
WORKDIR "/src/DysonNetwork.Gateway"
|
|
||||||
RUN dotnet build "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
|
||||||
|
|
||||||
FROM build AS publish
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
RUN dotnet publish "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
|
||||||
|
|
||||||
FROM base AS final
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
ENTRYPOINT ["dotnet", "DysonNetwork.Gateway.dll"]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="dotnet-etcd" Version="8.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
|
||||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
using DysonNetwork.Gateway.Startup;
|
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.Host.UseContentRoot(Directory.GetCurrentDirectory());
|
|
||||||
builder.WebHost.ConfigureKestrel(options =>
|
|
||||||
{
|
|
||||||
options.Limits.MaxRequestBodySize = long.MaxValue;
|
|
||||||
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
|
|
||||||
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add services to the container.
|
|
||||||
builder.Services.AddGateway(builder.Configuration);
|
|
||||||
builder.Services.AddControllers();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
|
||||||
{
|
|
||||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
|
||||||
});
|
|
||||||
|
|
||||||
app.UseRequestTimeouts();
|
|
||||||
app.UseCors(opts =>
|
|
||||||
opts.SetIsOriginAllowed(_ => true)
|
|
||||||
.WithExposedHeaders("*")
|
|
||||||
.WithHeaders("*")
|
|
||||||
.AllowCredentials()
|
|
||||||
.AllowAnyHeader()
|
|
||||||
.AllowAnyMethod()
|
|
||||||
);
|
|
||||||
|
|
||||||
app.MapControllers();
|
|
||||||
app.MapReverseProxy();
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
|
||||||
"profiles": {
|
|
||||||
"http": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": false,
|
|
||||||
"applicationUrl": "http://localhost:5094",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": false,
|
|
||||||
"applicationUrl": "https://localhost:7034;http://0.0.0.0:5094",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using dotnet_etcd.interfaces;
|
|
||||||
using Yarp.ReverseProxy.Configuration;
|
|
||||||
using Yarp.ReverseProxy.Forwarder;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Gateway;
|
|
||||||
|
|
||||||
public class RegistryProxyConfigProvider : IProxyConfigProvider, IDisposable
|
|
||||||
{
|
|
||||||
private readonly object _lock = new();
|
|
||||||
private readonly IEtcdClient _etcdClient;
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly ILogger<RegistryProxyConfigProvider> _logger;
|
|
||||||
private readonly CancellationTokenSource _watchCts = new();
|
|
||||||
private CancellationTokenSource _cts;
|
|
||||||
private IProxyConfig _config;
|
|
||||||
|
|
||||||
public RegistryProxyConfigProvider(
|
|
||||||
IEtcdClient etcdClient,
|
|
||||||
IConfiguration configuration,
|
|
||||||
ILogger<RegistryProxyConfigProvider> logger
|
|
||||||
)
|
|
||||||
{
|
|
||||||
_etcdClient = etcdClient;
|
|
||||||
_configuration = configuration;
|
|
||||||
_logger = logger;
|
|
||||||
_cts = new CancellationTokenSource();
|
|
||||||
_config = LoadConfig();
|
|
||||||
|
|
||||||
// Watch for changes in etcd
|
|
||||||
_etcdClient.WatchRange("/services/", _ =>
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Etcd configuration changed. Reloading proxy config.");
|
|
||||||
ReloadConfig();
|
|
||||||
}, cancellationToken: _watchCts.Token);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IProxyConfig GetConfig() => _config;
|
|
||||||
|
|
||||||
private void ReloadConfig()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
var oldCts = _cts;
|
|
||||||
_cts = new CancellationTokenSource();
|
|
||||||
_config = LoadConfig();
|
|
||||||
oldCts.Cancel();
|
|
||||||
oldCts.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IProxyConfig LoadConfig()
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Generating new proxy config.");
|
|
||||||
var response = _etcdClient.GetRange("/services/");
|
|
||||||
var kvs = response.Kvs;
|
|
||||||
|
|
||||||
var serviceMap = kvs.ToDictionary(
|
|
||||||
kv => Encoding.UTF8.GetString(kv.Key.ToByteArray()).Replace("/services/", ""),
|
|
||||||
kv => Encoding.UTF8.GetString(kv.Value.ToByteArray())
|
|
||||||
);
|
|
||||||
|
|
||||||
var clusters = new List<ClusterConfig>();
|
|
||||||
var routes = new List<RouteConfig>();
|
|
||||||
|
|
||||||
var domainMappings = _configuration.GetSection("DomainMappings").GetChildren()
|
|
||||||
.ToDictionary(x => x.Key, x => x.Value);
|
|
||||||
|
|
||||||
var pathAliases = _configuration.GetSection("PathAliases").GetChildren()
|
|
||||||
.ToDictionary(x => x.Key, x => x.Value);
|
|
||||||
|
|
||||||
var directRoutes = _configuration.GetSection("DirectRoutes").Get<List<DirectRouteConfig>>() ??
|
|
||||||
[];
|
|
||||||
|
|
||||||
_logger.LogInformation("Indexing {ServiceCount} services from Etcd.", kvs.Count);
|
|
||||||
|
|
||||||
var gatewayServiceName = _configuration["Service:Name"];
|
|
||||||
|
|
||||||
// Add direct route for /cgi to Gateway
|
|
||||||
var gatewayCluster = new ClusterConfig
|
|
||||||
{
|
|
||||||
ClusterId = "gateway-self",
|
|
||||||
Destinations = new Dictionary<string, DestinationConfig>
|
|
||||||
{
|
|
||||||
{ "self", new DestinationConfig { Address = _configuration["Kestrel:Endpoints:Http:Url"] ?? "http://localhost:5000" } }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
clusters.Add(gatewayCluster);
|
|
||||||
|
|
||||||
var cgiRoute = new RouteConfig
|
|
||||||
{
|
|
||||||
RouteId = "gateway-cgi-route",
|
|
||||||
ClusterId = "gateway-self",
|
|
||||||
Match = new RouteMatch { Path = "/cgi/{**catch-all}" }
|
|
||||||
};
|
|
||||||
routes.Add(cgiRoute);
|
|
||||||
_logger.LogInformation(" Added CGI Route: /cgi/** -> Gateway");
|
|
||||||
|
|
||||||
// Add direct routes
|
|
||||||
foreach (var directRoute in directRoutes)
|
|
||||||
{
|
|
||||||
if (serviceMap.TryGetValue(directRoute.Service, out var serviceUrl))
|
|
||||||
{
|
|
||||||
var existingCluster = clusters.FirstOrDefault(c => c.ClusterId == directRoute.Service);
|
|
||||||
if (existingCluster is null)
|
|
||||||
{
|
|
||||||
var cluster = new ClusterConfig
|
|
||||||
{
|
|
||||||
ClusterId = directRoute.Service,
|
|
||||||
Destinations = new Dictionary<string, DestinationConfig>
|
|
||||||
{
|
|
||||||
{ "destination1", new DestinationConfig { Address = serviceUrl } }
|
|
||||||
},
|
|
||||||
};
|
|
||||||
clusters.Add(cluster);
|
|
||||||
}
|
|
||||||
|
|
||||||
var route = new RouteConfig
|
|
||||||
{
|
|
||||||
RouteId = $"direct-{directRoute.Service}-{directRoute.Path.Replace("/", "-")}",
|
|
||||||
ClusterId = directRoute.Service,
|
|
||||||
Match = new RouteMatch { Path = directRoute.Path },
|
|
||||||
};
|
|
||||||
routes.Add(route);
|
|
||||||
_logger.LogInformation(" Added Direct Route: {Path} -> {Service}", directRoute.Path,
|
|
||||||
directRoute.Service);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning(" Direct route service {Service} not found in Etcd.", directRoute.Service);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var serviceName in serviceMap.Keys)
|
|
||||||
{
|
|
||||||
if (serviceName == gatewayServiceName)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Skipping gateway service: {ServiceName}", serviceName);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var serviceUrl = serviceMap[serviceName];
|
|
||||||
|
|
||||||
// Determine the path alias
|
|
||||||
string? pathAlias;
|
|
||||||
pathAlias = pathAliases.TryGetValue(serviceName, out var alias)
|
|
||||||
? alias
|
|
||||||
: serviceName.Split('.').Last().ToLowerInvariant();
|
|
||||||
|
|
||||||
_logger.LogInformation(" Service: {ServiceName}, URL: {ServiceUrl}, Path Alias: {PathAlias}", serviceName,
|
|
||||||
serviceUrl, pathAlias);
|
|
||||||
|
|
||||||
// Check if the cluster already exists
|
|
||||||
var existingCluster = clusters.FirstOrDefault(c => c.ClusterId == serviceName);
|
|
||||||
if (existingCluster == null)
|
|
||||||
{
|
|
||||||
var cluster = new ClusterConfig
|
|
||||||
{
|
|
||||||
ClusterId = serviceName,
|
|
||||||
Destinations = new Dictionary<string, DestinationConfig>
|
|
||||||
{
|
|
||||||
{ "destination1", new DestinationConfig { Address = serviceUrl } }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
clusters.Add(cluster);
|
|
||||||
_logger.LogInformation(" Added Cluster: {ServiceName}", serviceName);
|
|
||||||
}
|
|
||||||
else if (existingCluster.Destinations is not null)
|
|
||||||
{
|
|
||||||
// Create a new cluster with merged destinations
|
|
||||||
var newDestinations = new Dictionary<string, DestinationConfig>(existingCluster.Destinations)
|
|
||||||
{
|
|
||||||
{
|
|
||||||
$"destination{existingCluster.Destinations.Count + 1}",
|
|
||||||
new DestinationConfig { Address = serviceUrl }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var mergedCluster = new ClusterConfig
|
|
||||||
{
|
|
||||||
ClusterId = serviceName,
|
|
||||||
Destinations = newDestinations
|
|
||||||
};
|
|
||||||
|
|
||||||
// Replace the existing cluster with the merged one
|
|
||||||
var index = clusters.IndexOf(existingCluster);
|
|
||||||
clusters[index] = mergedCluster;
|
|
||||||
|
|
||||||
_logger.LogInformation(" Updated Cluster {ServiceName} with {DestinationCount} destinations",
|
|
||||||
serviceName, mergedCluster.Destinations.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Host-based routing
|
|
||||||
if (domainMappings.TryGetValue(serviceName, out var domain))
|
|
||||||
{
|
|
||||||
var hostRoute = new RouteConfig
|
|
||||||
{
|
|
||||||
RouteId = $"{serviceName}-host",
|
|
||||||
ClusterId = serviceName,
|
|
||||||
Match = new RouteMatch
|
|
||||||
{
|
|
||||||
Hosts = [domain],
|
|
||||||
Path = "/{**catch-all}"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
routes.Add(hostRoute);
|
|
||||||
_logger.LogInformation(" Added Host-based Route: {Host}", domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path-based routing
|
|
||||||
var pathRoute = new RouteConfig
|
|
||||||
{
|
|
||||||
RouteId = $"{serviceName}-path",
|
|
||||||
ClusterId = serviceName,
|
|
||||||
Match = new RouteMatch { Path = $"/{pathAlias}/{{**catch-all}}" },
|
|
||||||
Transforms = new List<Dictionary<string, string>>
|
|
||||||
{
|
|
||||||
new() { { "PathRemovePrefix", $"/{pathAlias}" } },
|
|
||||||
new() { { "PathPrefix", "/api" } }
|
|
||||||
},
|
|
||||||
Timeout = TimeSpan.FromSeconds(5)
|
|
||||||
};
|
|
||||||
routes.Add(pathRoute);
|
|
||||||
_logger.LogInformation(" Added Path-based Route: {Path}", pathRoute.Match.Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CustomProxyConfig(
|
|
||||||
routes,
|
|
||||||
clusters,
|
|
||||||
new Microsoft.Extensions.Primitives.CancellationChangeToken(_cts.Token)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CustomProxyConfig(
|
|
||||||
IReadOnlyList<RouteConfig> routes,
|
|
||||||
IReadOnlyList<ClusterConfig> clusters,
|
|
||||||
Microsoft.Extensions.Primitives.IChangeToken changeToken
|
|
||||||
)
|
|
||||||
: IProxyConfig
|
|
||||||
{
|
|
||||||
public IReadOnlyList<RouteConfig> Routes { get; } = routes;
|
|
||||||
public IReadOnlyList<ClusterConfig> Clusters { get; } = clusters;
|
|
||||||
public Microsoft.Extensions.Primitives.IChangeToken ChangeToken { get; } = changeToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public record DirectRouteConfig
|
|
||||||
{
|
|
||||||
public required string Path { get; set; }
|
|
||||||
public required string Service { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void Dispose()
|
|
||||||
{
|
|
||||||
_cts.Cancel();
|
|
||||||
_cts.Dispose();
|
|
||||||
_watchCts.Cancel();
|
|
||||||
_watchCts.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using System.Net.Security;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using DysonNetwork.Shared.Registry;
|
|
||||||
using Yarp.ReverseProxy.Configuration;
|
|
||||||
using Yarp.ReverseProxy.Transforms;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Gateway.Startup;
|
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddGateway(this IServiceCollection services, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
services.AddRequestTimeouts();
|
|
||||||
|
|
||||||
services
|
|
||||||
.AddReverseProxy()
|
|
||||||
.ConfigureHttpClient((context, handler) =>
|
|
||||||
{
|
|
||||||
// var caCert = X509CertificateLoader.LoadCertificateFromFile(configuration["CaCert"]!);
|
|
||||||
handler.SslOptions = new SslClientAuthenticationOptions
|
|
||||||
{
|
|
||||||
RemoteCertificateValidationCallback = (sender, cert, chain, errors) => true
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.AddTransforms(context =>
|
|
||||||
{
|
|
||||||
context.AddForwarded();
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddRegistryService(configuration, addForwarder: false);
|
|
||||||
services.AddSingleton<IProxyConfigProvider, RegistryProxyConfigProvider>();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using DysonNetwork.Shared.Data;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Gateway;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("/api/version")]
|
|
||||||
public class VersionController : ControllerBase
|
|
||||||
{
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult Get()
|
|
||||||
{
|
|
||||||
return Ok(new AppVersion
|
|
||||||
{
|
|
||||||
Version = ThisAssembly.AssemblyVersion,
|
|
||||||
Commit = ThisAssembly.GitCommitId,
|
|
||||||
UpdateDate = ThisAssembly.GitCommitDate
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"LocalMode": true,
|
|
||||||
"CaCert": "../Certificates/ca.crt",
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"Etcd": "etcd.orb.local:2379"
|
|
||||||
},
|
|
||||||
"Etcd": {
|
|
||||||
"Insecure": true
|
|
||||||
},
|
|
||||||
"Service": {
|
|
||||||
"Name": "DysonNetwork.Gateway",
|
|
||||||
"Url": "https://localhost:7034"
|
|
||||||
},
|
|
||||||
"DomainMappings": {
|
|
||||||
"DysonNetwork.Pass": "id.solsynth.dev",
|
|
||||||
"DysonNetwork.Drive": "drive.solsynth.dev",
|
|
||||||
"DysonNetwork.Pusher": "push.solsynth.dev",
|
|
||||||
"DysonNetwork.Sphere": "sphere.solsynth.dev"
|
|
||||||
},
|
|
||||||
"PathAliases": {
|
|
||||||
"DysonNetwork.Pass": "id",
|
|
||||||
"DysonNetwork.Drive": "drive"
|
|
||||||
},
|
|
||||||
"DirectRoutes": [
|
|
||||||
{
|
|
||||||
"Path": "/ws",
|
|
||||||
"Service": "DysonNetwork.Pusher"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Path": "/api/tus",
|
|
||||||
"Service": "DysonNetwork.Drive"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Path": "/.well-known/openid-configuration",
|
|
||||||
"Service": "DysonNetwork.Pass"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Path": "/.well-known/jwks",
|
|
||||||
"Service": "DysonNetwork.Pass"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0",
|
|
||||||
"publicReleaseRefSpec": ["^refs/heads/main$"],
|
|
||||||
"cloudBuild": {
|
|
||||||
"setVersionVariables": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ public class Account : ModelBase
|
|||||||
[MaxLength(256)] public string Name { get; set; } = string.Empty;
|
[MaxLength(256)] public string Name { get; set; } = string.Empty;
|
||||||
[MaxLength(256)] public string Nick { get; set; } = string.Empty;
|
[MaxLength(256)] public string Nick { get; set; } = string.Empty;
|
||||||
[MaxLength(32)] public string Language { get; set; } = string.Empty;
|
[MaxLength(32)] public string Language { get; set; } = string.Empty;
|
||||||
|
[MaxLength(32)] public string Region { get; set; } = string.Empty;
|
||||||
public Instant? ActivatedAt { get; set; }
|
public Instant? ActivatedAt { get; set; }
|
||||||
public bool IsSuperuser { get; set; } = false;
|
public bool IsSuperuser { get; set; } = false;
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ public class Account : ModelBase
|
|||||||
Name = Name,
|
Name = Name,
|
||||||
Nick = Nick,
|
Nick = Nick,
|
||||||
Language = Language,
|
Language = Language,
|
||||||
|
Region = Region,
|
||||||
ActivatedAt = ActivatedAt?.ToTimestamp(),
|
ActivatedAt = ActivatedAt?.ToTimestamp(),
|
||||||
IsSuperuser = IsSuperuser,
|
IsSuperuser = IsSuperuser,
|
||||||
Profile = Profile.ToProtoValue(),
|
Profile = Profile.ToProtoValue(),
|
||||||
@@ -75,6 +77,7 @@ public class Account : ModelBase
|
|||||||
Name = proto.Name,
|
Name = proto.Name,
|
||||||
Nick = proto.Nick,
|
Nick = proto.Nick,
|
||||||
Language = proto.Language,
|
Language = proto.Language,
|
||||||
|
Region = proto.Region,
|
||||||
ActivatedAt = proto.ActivatedAt?.ToInstant(),
|
ActivatedAt = proto.ActivatedAt?.ToInstant(),
|
||||||
IsSuperuser = proto.IsSuperuser,
|
IsSuperuser = proto.IsSuperuser,
|
||||||
PerkSubscription = proto.PerkSubscription is not null
|
PerkSubscription = proto.PerkSubscription is not null
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using DysonNetwork.Pass.Auth;
|
|||||||
using DysonNetwork.Pass.Credit;
|
using DysonNetwork.Pass.Credit;
|
||||||
using DysonNetwork.Pass.Wallet;
|
using DysonNetwork.Pass.Wallet;
|
||||||
using DysonNetwork.Shared.Error;
|
using DysonNetwork.Shared.Error;
|
||||||
|
using DysonNetwork.Shared.GeoIp;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
@@ -17,7 +18,8 @@ public class AccountController(
|
|||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
SubscriptionService subscriptions,
|
SubscriptionService subscriptions,
|
||||||
AccountEventService events,
|
AccountEventService events,
|
||||||
SocialCreditService socialCreditService
|
SocialCreditService socialCreditService,
|
||||||
|
GeoIpService geo
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("{name}")]
|
[HttpGet("{name}")]
|
||||||
@@ -32,10 +34,10 @@ public class AccountController(
|
|||||||
.Where(a => a.Name == name)
|
.Where(a => a.Name == name)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (account is null) return NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier));
|
if (account is null) return NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier));
|
||||||
|
|
||||||
var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id);
|
var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id);
|
||||||
account.PerkSubscription = perk?.ToReference();
|
account.PerkSubscription = perk?.ToReference();
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,9 +50,11 @@ public class AccountController(
|
|||||||
.Include(e => e.Badges)
|
.Include(e => e.Badges)
|
||||||
.Where(a => a.Name == name)
|
.Where(a => a.Name == name)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
return account is null ? NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier)) : account.Badges.ToList();
|
return account is null
|
||||||
|
? NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier))
|
||||||
|
: account.Badges.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{name}/credits")]
|
[HttpGet("{name}/credits")]
|
||||||
[ProducesResponseType<double>(StatusCodes.Status200OK)]
|
[ProducesResponseType<double>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
@@ -60,12 +64,12 @@ public class AccountController(
|
|||||||
.Where(a => a.Name == name)
|
.Where(a => a.Name == name)
|
||||||
.Select(a => new { a.Id })
|
.Select(a => new { a.Id })
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (account is null)
|
if (account is null)
|
||||||
{
|
{
|
||||||
return NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier));
|
return NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
var credits = await socialCreditService.GetSocialCredit(account.Id);
|
var credits = await socialCreditService.GetSocialCredit(account.Id);
|
||||||
return credits;
|
return credits;
|
||||||
}
|
}
|
||||||
@@ -93,7 +97,7 @@ public class AccountController(
|
|||||||
[MaxLength(128)]
|
[MaxLength(128)]
|
||||||
public string Password { get; set; } = string.Empty;
|
public string Password { get; set; } = string.Empty;
|
||||||
|
|
||||||
[MaxLength(128)] public string Language { get; set; } = "en-us";
|
[MaxLength(32)] public string Language { get; set; } = "en-us";
|
||||||
|
|
||||||
[Required] public string CaptchaToken { get; set; } = string.Empty;
|
[Required] public string CaptchaToken { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -109,6 +113,10 @@ public class AccountController(
|
|||||||
[nameof(request.CaptchaToken)] = ["Invalid captcha token."]
|
[nameof(request.CaptchaToken)] = ["Invalid captcha token."]
|
||||||
}, traceId: HttpContext.TraceIdentifier));
|
}, traceId: HttpContext.TraceIdentifier));
|
||||||
|
|
||||||
|
var ip = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||||
|
if (ip is null) return BadRequest(ApiError.NotFound(request.Name, traceId: HttpContext.TraceIdentifier));
|
||||||
|
var region = geo.GetFromIp(ip)?.Country.IsoCode ?? "us";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var account = await accounts.CreateAccount(
|
var account = await accounts.CreateAccount(
|
||||||
@@ -116,7 +124,8 @@ public class AccountController(
|
|||||||
request.Nick,
|
request.Nick,
|
||||||
request.Email,
|
request.Email,
|
||||||
request.Password,
|
request.Password,
|
||||||
request.Language
|
request.Language,
|
||||||
|
region
|
||||||
);
|
);
|
||||||
return Ok(account);
|
return Ok(account);
|
||||||
}
|
}
|
||||||
@@ -182,7 +191,9 @@ public class AccountController(
|
|||||||
public StatusAttitude Attitude { get; set; }
|
public StatusAttitude Attitude { get; set; }
|
||||||
public bool IsInvisible { get; set; }
|
public bool IsInvisible { get; set; }
|
||||||
public bool IsNotDisturb { get; set; }
|
public bool IsNotDisturb { get; set; }
|
||||||
|
public bool IsAutomated { get; set; } = false;
|
||||||
[MaxLength(1024)] public string? Label { get; set; }
|
[MaxLength(1024)] public string? Label { get; set; }
|
||||||
|
[MaxLength(4096)] public string? AppIdentifier { get; set; }
|
||||||
public Instant? ClearedAt { get; set; }
|
public Instant? ClearedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ public class AccountCurrentController(
|
|||||||
{
|
{
|
||||||
[MaxLength(256)] public string? Nick { get; set; }
|
[MaxLength(256)] public string? Nick { get; set; }
|
||||||
[MaxLength(32)] public string? Language { get; set; }
|
[MaxLength(32)] public string? Language { get; set; }
|
||||||
|
[MaxLength(32)] public string? Region { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch]
|
[HttpPatch]
|
||||||
@@ -63,6 +64,7 @@ public class AccountCurrentController(
|
|||||||
|
|
||||||
if (request.Nick is not null) account.Nick = request.Nick;
|
if (request.Nick is not null) account.Nick = request.Nick;
|
||||||
if (request.Language is not null) account.Language = request.Language;
|
if (request.Language is not null) account.Language = request.Language;
|
||||||
|
if (request.Region is not null) account.Region = request.Region;
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
await accounts.PurgeAccountCache(currentUser);
|
await accounts.PurgeAccountCache(currentUser);
|
||||||
@@ -195,6 +197,8 @@ public class AccountCurrentController(
|
|||||||
public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request)
|
public async Task<ActionResult<Status>> UpdateStatus([FromBody] AccountController.StatusRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
if (request is { IsAutomated: true, AppIdentifier: not null })
|
||||||
|
return BadRequest("Automated status cannot be updated.");
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var status = await db.AccountStatuses
|
var status = await db.AccountStatuses
|
||||||
@@ -203,11 +207,15 @@ public class AccountCurrentController(
|
|||||||
.OrderByDescending(e => e.CreatedAt)
|
.OrderByDescending(e => e.CreatedAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (status is null) return NotFound(ApiError.NotFound("status", traceId: HttpContext.TraceIdentifier));
|
if (status is null) return NotFound(ApiError.NotFound("status", traceId: HttpContext.TraceIdentifier));
|
||||||
|
if (status.IsAutomated && request.AppIdentifier is null)
|
||||||
|
return BadRequest("Automated status cannot be updated.");
|
||||||
|
|
||||||
status.Attitude = request.Attitude;
|
status.Attitude = request.Attitude;
|
||||||
status.IsInvisible = request.IsInvisible;
|
status.IsInvisible = request.IsInvisible;
|
||||||
status.IsNotDisturb = request.IsNotDisturb;
|
status.IsNotDisturb = request.IsNotDisturb;
|
||||||
|
status.IsAutomated = request.IsAutomated;
|
||||||
status.Label = request.Label;
|
status.Label = request.Label;
|
||||||
|
status.AppIdentifier = request.AppIdentifier;
|
||||||
status.ClearedAt = request.ClearedAt;
|
status.ClearedAt = request.ClearedAt;
|
||||||
|
|
||||||
db.Update(status);
|
db.Update(status);
|
||||||
@@ -223,13 +231,44 @@ public class AccountCurrentController(
|
|||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
if (request is { IsAutomated: true, AppIdentifier: not null })
|
||||||
|
{
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
var existingStatus = await db.AccountStatuses
|
||||||
|
.Where(s => s.AccountId == currentUser.Id)
|
||||||
|
.Where(s => s.ClearedAt == null || s.ClearedAt > now)
|
||||||
|
.OrderByDescending(s => s.CreatedAt)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (existingStatus is not null && existingStatus.IsAutomated)
|
||||||
|
if (existingStatus.IsAutomated && request.AppIdentifier == existingStatus.AppIdentifier)
|
||||||
|
{
|
||||||
|
existingStatus.Attitude = request.Attitude;
|
||||||
|
existingStatus.IsInvisible = request.IsInvisible;
|
||||||
|
existingStatus.IsNotDisturb = request.IsNotDisturb;
|
||||||
|
existingStatus.Label = request.Label;
|
||||||
|
db.Update(existingStatus);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return Ok(existingStatus);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingStatus.ClearedAt = now;
|
||||||
|
db.Update(existingStatus);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
else if (existingStatus is not null)
|
||||||
|
return Ok(existingStatus); // Do not override manually set status with automated ones
|
||||||
|
}
|
||||||
|
|
||||||
var status = new Status
|
var status = new Status
|
||||||
{
|
{
|
||||||
AccountId = currentUser.Id,
|
AccountId = currentUser.Id,
|
||||||
Attitude = request.Attitude,
|
Attitude = request.Attitude,
|
||||||
IsInvisible = request.IsInvisible,
|
IsInvisible = request.IsInvisible,
|
||||||
IsNotDisturb = request.IsNotDisturb,
|
IsNotDisturb = request.IsNotDisturb,
|
||||||
|
IsAutomated = request.IsAutomated,
|
||||||
Label = request.Label,
|
Label = request.Label,
|
||||||
|
AppIdentifier = request.AppIdentifier,
|
||||||
ClearedAt = request.ClearedAt
|
ClearedAt = request.ClearedAt
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,15 +276,21 @@ public class AccountCurrentController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("statuses")]
|
[HttpDelete("statuses")]
|
||||||
public async Task<ActionResult> DeleteStatus()
|
public async Task<ActionResult> DeleteStatus([FromQuery] string? app)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var status = await db.AccountStatuses
|
var queryable = db.AccountStatuses
|
||||||
.Where(s => s.AccountId == currentUser.Id)
|
.Where(s => s.AccountId == currentUser.Id)
|
||||||
.Where(s => s.ClearedAt == null || s.ClearedAt > now)
|
.Where(s => s.ClearedAt == null || s.ClearedAt > now)
|
||||||
.OrderByDescending(s => s.CreatedAt)
|
.OrderByDescending(s => s.CreatedAt)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(app))
|
||||||
|
queryable = queryable.Where(s => s.IsAutomated && s.AppIdentifier == app);
|
||||||
|
|
||||||
|
var status = await queryable
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (status is null) return NotFound();
|
if (status is null) return NotFound();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class AccountEventService(
|
|||||||
Wallet.PaymentService payment,
|
Wallet.PaymentService payment,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
IStringLocalizer<Localization.AccountEventResource> localizer,
|
IStringLocalizer<Localization.AccountEventResource> localizer,
|
||||||
PusherService.PusherServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
SubscriptionService subscriptions,
|
SubscriptionService subscriptions,
|
||||||
Pass.Leveling.ExperienceService experienceService
|
Pass.Leveling.ExperienceService experienceService
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ using EFCore.BulkExtensions;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using NATS.Client.Core;
|
using NATS.Client.Core;
|
||||||
|
using NATS.Client.JetStream;
|
||||||
|
using NATS.Net;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using OtpNet;
|
using OtpNet;
|
||||||
using AuthService = DysonNetwork.Pass.Auth.AuthService;
|
using AuthService = DysonNetwork.Pass.Auth.AuthService;
|
||||||
@@ -26,8 +28,9 @@ public class AccountService(
|
|||||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||||
AccountUsernameService uname,
|
AccountUsernameService uname,
|
||||||
EmailService mailer,
|
EmailService mailer,
|
||||||
PusherService.PusherServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
|
IStringLocalizer<EmailResource> emailLocalizer,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
ILogger<AccountService> logger,
|
ILogger<AccountService> logger,
|
||||||
INatsConnection nats
|
INatsConnection nats
|
||||||
@@ -87,6 +90,7 @@ public class AccountService(
|
|||||||
string email,
|
string email,
|
||||||
string? password,
|
string? password,
|
||||||
string language = "en-US",
|
string language = "en-US",
|
||||||
|
string region = "en",
|
||||||
bool isEmailVerified = false,
|
bool isEmailVerified = false,
|
||||||
bool isActivated = false
|
bool isActivated = false
|
||||||
)
|
)
|
||||||
@@ -106,6 +110,7 @@ public class AccountService(
|
|||||||
Name = name,
|
Name = name,
|
||||||
Nick = nick,
|
Nick = nick,
|
||||||
Language = language,
|
Language = language,
|
||||||
|
Region = region,
|
||||||
Contacts = new List<AccountContact>
|
Contacts = new List<AccountContact>
|
||||||
{
|
{
|
||||||
new()
|
new()
|
||||||
@@ -180,12 +185,14 @@ public class AccountService(
|
|||||||
userInfo.Email,
|
userInfo.Email,
|
||||||
null,
|
null,
|
||||||
"en-US",
|
"en-US",
|
||||||
|
"en",
|
||||||
userInfo.EmailVerified,
|
userInfo.EmailVerified,
|
||||||
userInfo.EmailVerified
|
userInfo.EmailVerified
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Account> CreateBotAccount(Account account, Guid automatedId, string? pictureId, string? backgroundId)
|
public async Task<Account> CreateBotAccount(Account account, Guid automatedId, string? pictureId,
|
||||||
|
string? backgroundId)
|
||||||
{
|
{
|
||||||
var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync();
|
var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync();
|
||||||
if (dupeAutomateCount > 0)
|
if (dupeAutomateCount > 0)
|
||||||
@@ -226,7 +233,7 @@ public class AccountService(
|
|||||||
);
|
);
|
||||||
account.Profile.Background = CloudFileReferenceObject.FromProtoValue(file);
|
account.Profile.Background = CloudFileReferenceObject.FromProtoValue(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Accounts.Add(account);
|
db.Accounts.Add(account);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
@@ -432,12 +439,14 @@ public class AccountService(
|
|||||||
.Where(c => c.Type == AccountContactType.Email)
|
.Where(c => c.Type == AccountContactType.Email)
|
||||||
.Where(c => c.VerifiedAt != null)
|
.Where(c => c.VerifiedAt != null)
|
||||||
.Where(c => c.IsPrimary)
|
.Where(c => c.IsPrimary)
|
||||||
|
.Where(c => c.AccountId == account.Id)
|
||||||
.Include(c => c.Account)
|
.Include(c => c.Account)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (contact is null)
|
if (contact is null)
|
||||||
{
|
{
|
||||||
logger.LogWarning(
|
logger.LogWarning(
|
||||||
"Unable to send factor code to #{FactorId} with, due to no contact method was found..."
|
"Unable to send factor code to #{FactorId} with, due to no contact method was found...",
|
||||||
|
factor.Id
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -446,7 +455,7 @@ public class AccountService(
|
|||||||
.SendTemplatedEmailAsync<Pages.Emails.VerificationEmail, VerificationEmailModel>(
|
.SendTemplatedEmailAsync<Pages.Emails.VerificationEmail, VerificationEmailModel>(
|
||||||
account.Nick,
|
account.Nick,
|
||||||
contact.Content,
|
contact.Content,
|
||||||
localizer["VerificationEmail"],
|
emailLocalizer["VerificationEmail"],
|
||||||
new VerificationEmailModel
|
new VerificationEmailModel
|
||||||
{
|
{
|
||||||
Name = account.Name,
|
Name = account.Name,
|
||||||
@@ -734,10 +743,14 @@ public class AccountService(
|
|||||||
db.Accounts.Remove(account);
|
db.Accounts.Remove(account);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await nats.PublishAsync(AccountDeletedEvent.Type, JsonSerializer.SerializeToUtf8Bytes(new AccountDeletedEvent
|
var js = nats.CreateJetStreamContext();
|
||||||
{
|
await js.PublishAsync(
|
||||||
AccountId = account.Id,
|
AccountDeletedEvent.Type,
|
||||||
DeletedAt = SystemClock.Instance.GetCurrentInstant()
|
GrpcTypeHelper.ConvertObjectToByteString(new AccountDeletedEvent
|
||||||
}));
|
{
|
||||||
|
AccountId = account.Id,
|
||||||
|
DeletedAt = SystemClock.Instance.GetCurrentInstant()
|
||||||
|
}).ToByteArray()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
|
using DysonNetwork.Shared.GeoIp;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using NodaTime.Serialization.Protobuf;
|
using NodaTime.Serialization.Protobuf;
|
||||||
using Point = NetTopologySuite.Geometries.Point;
|
using Point = NetTopologySuite.Geometries.Point;
|
||||||
@@ -14,7 +16,7 @@ public class ActionLog : ModelBase
|
|||||||
[Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new();
|
[Column(TypeName = "jsonb")] public Dictionary<string, object> Meta { get; set; } = new();
|
||||||
[MaxLength(512)] public string? UserAgent { get; set; }
|
[MaxLength(512)] public string? UserAgent { get; set; }
|
||||||
[MaxLength(128)] public string? IpAddress { get; set; }
|
[MaxLength(128)] public string? IpAddress { get; set; }
|
||||||
public Point? Location { get; set; }
|
[Column(TypeName = "jsonb")] public GeoPoint? Location { get; set; }
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
public Account Account { get; set; } = null!;
|
public Account Account { get; set; } = null!;
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ public class Status : ModelBase
|
|||||||
public bool IsNotDisturb { get; set; }
|
public bool IsNotDisturb { get; set; }
|
||||||
[MaxLength(1024)] public string? Label { get; set; }
|
[MaxLength(1024)] public string? Label { get; set; }
|
||||||
public Instant? ClearedAt { get; set; }
|
public Instant? ClearedAt { get; set; }
|
||||||
|
[MaxLength(4096)] public string? AppIdentifier { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates this status is created based on running process or rich presence
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAutomated { get; set; }
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
public Account Account { get; set; } = null!;
|
public Account Account { get; set; } = null!;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
|||||||
using DysonNetwork.Pass.Email;
|
using DysonNetwork.Pass.Email;
|
||||||
using DysonNetwork.Pass.Pages.Emails;
|
using DysonNetwork.Pass.Pages.Emails;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
@@ -15,7 +16,8 @@ public class MagicSpellService(
|
|||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<MagicSpellService> logger,
|
ILogger<MagicSpellService> logger,
|
||||||
IStringLocalizer<EmailResource> localizer,
|
IStringLocalizer<EmailResource> localizer,
|
||||||
EmailService email
|
EmailService email,
|
||||||
|
ICacheService cache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public async Task<MagicSpell> CreateMagicSpell(
|
public async Task<MagicSpell> CreateMagicSpell(
|
||||||
@@ -35,11 +37,8 @@ public class MagicSpellService(
|
|||||||
.Where(s => s.Type == type)
|
.Where(s => s.Type == type)
|
||||||
.Where(s => s.ExpiresAt == null || s.ExpiresAt > now)
|
.Where(s => s.ExpiresAt == null || s.ExpiresAt > now)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
if (existingSpell is not null)
|
||||||
if (existingSpell != null)
|
return existingSpell;
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Account already has an active magic spell of type {type}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var spellWord = _GenerateRandomString(128);
|
var spellWord = _GenerateRandomString(128);
|
||||||
@@ -59,8 +58,18 @@ public class MagicSpellService(
|
|||||||
return spell;
|
return spell;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const string SpellNotifyCacheKeyPrefix = "spells:notify:";
|
||||||
|
|
||||||
public async Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false)
|
public async Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false)
|
||||||
{
|
{
|
||||||
|
var cacheKey = SpellNotifyCacheKeyPrefix + spell.Id;
|
||||||
|
var (found, _) = await cache.GetAsyncWithStatus<bool?>(cacheKey);
|
||||||
|
if (found)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Skip sending magic spell {SpellId} due to already sent.", spell.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var contact = await db.AccountContacts
|
var contact = await db.AccountContacts
|
||||||
.Where(c => c.Account.Id == spell.AccountId)
|
.Where(c => c.Account.Id == spell.AccountId)
|
||||||
.Where(c => c.Type == AccountContactType.Email)
|
.Where(c => c.Type == AccountContactType.Email)
|
||||||
@@ -112,7 +121,7 @@ public class MagicSpellService(
|
|||||||
await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
|
await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
|
||||||
contact.Account.Nick,
|
contact.Account.Nick,
|
||||||
contact.Content,
|
contact.Content,
|
||||||
localizer["EmailAccountDeletionTitle"],
|
localizer["EmailPasswordResetTitle"],
|
||||||
new PasswordResetEmailModel
|
new PasswordResetEmailModel
|
||||||
{
|
{
|
||||||
Name = contact.Account.Name,
|
Name = contact.Account.Name,
|
||||||
@@ -138,6 +147,8 @@ public class MagicSpellService(
|
|||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await cache.SetAsync(cacheKey, true, TimeSpan.FromMinutes(5));
|
||||||
}
|
}
|
||||||
catch (Exception err)
|
catch (Exception err)
|
||||||
{
|
{
|
||||||
|
|||||||
53
DysonNetwork.Pass/Account/NotableDay.cs
Normal file
53
DysonNetwork.Pass/Account/NotableDay.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using Nager.Holiday;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reference from Nager.Holiday
|
||||||
|
/// </summary>
|
||||||
|
public enum NotableHolidayType
|
||||||
|
{
|
||||||
|
/// <summary>Public holiday</summary>
|
||||||
|
Public,
|
||||||
|
/// <summary>Bank holiday, banks and offices are closed</summary>
|
||||||
|
Bank,
|
||||||
|
/// <summary>School holiday, schools are closed</summary>
|
||||||
|
School,
|
||||||
|
/// <summary>Authorities are closed</summary>
|
||||||
|
Authorities,
|
||||||
|
/// <summary>Majority of people take a day off</summary>
|
||||||
|
Optional,
|
||||||
|
/// <summary>Optional festivity, no paid day off</summary>
|
||||||
|
Observance,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class NotableDay
|
||||||
|
{
|
||||||
|
public Instant Date { get; set; }
|
||||||
|
public string? LocalName { get; set; }
|
||||||
|
public string? GlobalName { get; set; }
|
||||||
|
public string? CountryCode { get; set; }
|
||||||
|
public NotableHolidayType[] Holidays { get; set; } = [];
|
||||||
|
|
||||||
|
public static NotableDay FromNagerHoliday(PublicHoliday holiday)
|
||||||
|
{
|
||||||
|
return new NotableDay()
|
||||||
|
{
|
||||||
|
Date = Instant.FromDateTimeUtc(holiday.Date.ToUniversalTime()),
|
||||||
|
LocalName = holiday.LocalName,
|
||||||
|
GlobalName = holiday.Name,
|
||||||
|
CountryCode = holiday.CountryCode,
|
||||||
|
Holidays = holiday.Types?.Select(x => x switch
|
||||||
|
{
|
||||||
|
PublicHolidayType.Public => NotableHolidayType.Public,
|
||||||
|
PublicHolidayType.Bank => NotableHolidayType.Bank,
|
||||||
|
PublicHolidayType.School => NotableHolidayType.School,
|
||||||
|
PublicHolidayType.Authorities => NotableHolidayType.Authorities,
|
||||||
|
PublicHolidayType.Optional => NotableHolidayType.Optional,
|
||||||
|
_ => NotableHolidayType.Observance
|
||||||
|
}).ToArray() ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
79
DysonNetwork.Pass/Account/NotableDaysController.cs
Normal file
79
DysonNetwork.Pass/Account/NotableDaysController.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/api/notable")]
|
||||||
|
public class NotableDaysController(NotableDaysService days) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("{regionCode}/{year:int}")]
|
||||||
|
public async Task<ActionResult<List<NotableDay>>> GetRegionDays(string regionCode, int year)
|
||||||
|
{
|
||||||
|
var result = await days.GetNotableDays(year, regionCode);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{regionCode}")]
|
||||||
|
public async Task<ActionResult<List<NotableDay>>> GetRegionDaysCurrentYear(string regionCode)
|
||||||
|
{
|
||||||
|
var currentYear = DateTime.Now.Year;
|
||||||
|
var result = await days.GetNotableDays(currentYear, regionCode);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("me/{year:int}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<List<NotableDay>>> GetAccountNotableDays(int year)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var region = currentUser.Region;
|
||||||
|
if (string.IsNullOrWhiteSpace(region)) region = "us";
|
||||||
|
|
||||||
|
var result = await days.GetNotableDays(year, region);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("me")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<List<NotableDay>>> GetAccountNotableDaysCurrentYear()
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var currentYear = DateTime.Now.Year;
|
||||||
|
var region = currentUser.Region;
|
||||||
|
if (string.IsNullOrWhiteSpace(region)) region = "us";
|
||||||
|
|
||||||
|
var result = await days.GetNotableDays(currentYear, region);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{regionCode}/next")]
|
||||||
|
public async Task<ActionResult<NotableDay?>> GetNextHoliday(string regionCode)
|
||||||
|
{
|
||||||
|
var result = await days.GetNextHoliday(regionCode);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return NotFound("No upcoming holidays found");
|
||||||
|
}
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("me/next")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<NotableDay?>> GetAccountNextHoliday()
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var region = currentUser.Region;
|
||||||
|
if (string.IsNullOrWhiteSpace(region)) region = "us";
|
||||||
|
|
||||||
|
var result = await days.GetNextHoliday(region);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return NotFound("No upcoming holidays found");
|
||||||
|
}
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
DysonNetwork.Pass/Account/NotableDaysService.cs
Normal file
55
DysonNetwork.Pass/Account/NotableDaysService.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using DysonNetwork.Shared.Cache;
|
||||||
|
using Nager.Holiday;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Account;
|
||||||
|
|
||||||
|
public class NotableDaysService(ICacheService cache)
|
||||||
|
{
|
||||||
|
private const string NotableDaysCacheKeyPrefix = "notable:";
|
||||||
|
|
||||||
|
public async Task<List<NotableDay>> GetNotableDays(int? year, string regionCode)
|
||||||
|
{
|
||||||
|
year ??= DateTime.UtcNow.Year;
|
||||||
|
|
||||||
|
// Generate cache key using year and region code
|
||||||
|
var cacheKey = $"{NotableDaysCacheKeyPrefix}:{year}:{regionCode}";
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
var (found, cachedDays) = await cache.GetAsyncWithStatus<List<NotableDay>>(cacheKey);
|
||||||
|
if (found && cachedDays != null)
|
||||||
|
{
|
||||||
|
return cachedDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in cache, fetch from API
|
||||||
|
using var holidayClient = new HolidayClient();
|
||||||
|
var holidays = await holidayClient.GetHolidaysAsync(year.Value, regionCode);
|
||||||
|
var days = holidays?.Select(NotableDay.FromNagerHoliday).ToList() ?? [];
|
||||||
|
|
||||||
|
// Cache the result for 1 day (holiday data doesn't change frequently)
|
||||||
|
await cache.SetAsync(cacheKey, days, TimeSpan.FromDays(1));
|
||||||
|
|
||||||
|
return days;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<NotableDay?> GetNextHoliday(string regionCode)
|
||||||
|
{
|
||||||
|
var currentDate = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
var currentYear = currentDate.InUtc().Year;
|
||||||
|
|
||||||
|
// Get holidays for current year and next year to cover all possibilities
|
||||||
|
var currentYearHolidays = await GetNotableDays(currentYear, regionCode);
|
||||||
|
var nextYearHolidays = await GetNotableDays(currentYear + 1, regionCode);
|
||||||
|
|
||||||
|
var allHolidays = currentYearHolidays.Concat(nextYearHolidays);
|
||||||
|
|
||||||
|
// Find the first holiday that is today or in the future
|
||||||
|
var nextHoliday = allHolidays
|
||||||
|
.Where(day => day.Date >= currentDate)
|
||||||
|
.OrderBy(day => day.Date)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
return nextHoliday;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ namespace DysonNetwork.Pass.Account;
|
|||||||
public class RelationshipService(
|
public class RelationshipService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
PusherService.PusherServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer
|
IStringLocalizer<NotificationResource> localizer
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Pass.Account;
|
using DysonNetwork.Pass.Account;
|
||||||
using DysonNetwork.Pass.Auth;
|
using DysonNetwork.Pass.Auth;
|
||||||
using DysonNetwork.Pass.Credit;
|
using DysonNetwork.Pass.Credit;
|
||||||
@@ -50,7 +52,7 @@ public class AppDatabase(
|
|||||||
public DbSet<Coupon> WalletCoupons { get; set; } = null!;
|
public DbSet<Coupon> WalletCoupons { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<Punishment> Punishments { get; set; } = null!;
|
public DbSet<Punishment> Punishments { get; set; } = null!;
|
||||||
|
|
||||||
public DbSet<SocialCreditRecord> SocialCreditRecords { get; set; } = null!;
|
public DbSet<SocialCreditRecord> SocialCreditRecords { get; set; } = null!;
|
||||||
public DbSet<ExperienceRecord> ExperienceRecords { get; set; } = null!;
|
public DbSet<ExperienceRecord> ExperienceRecords { get; set; } = null!;
|
||||||
|
|
||||||
@@ -59,9 +61,14 @@ public class AppDatabase(
|
|||||||
optionsBuilder.UseNpgsql(
|
optionsBuilder.UseNpgsql(
|
||||||
configuration.GetConnectionString("App"),
|
configuration.GetConnectionString("App"),
|
||||||
opt => opt
|
opt => opt
|
||||||
.ConfigureDataSource(optSource => optSource.EnableDynamicJson())
|
.ConfigureDataSource(optSource => optSource
|
||||||
|
.EnableDynamicJson()
|
||||||
|
.ConfigureJsonOptions(new JsonSerializerOptions()
|
||||||
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
|
})
|
||||||
|
)
|
||||||
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
|
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
|
||||||
.UseNetTopologySuite()
|
|
||||||
.UseNodaTime()
|
.UseNodaTime()
|
||||||
).UseSnakeCaseNamingConvention();
|
).UseSnakeCaseNamingConvention();
|
||||||
|
|
||||||
@@ -276,4 +283,4 @@ public static class OptionalQueryExtensions
|
|||||||
{
|
{
|
||||||
return condition ? transform(source) : source;
|
return condition ? transform(source) : source;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,10 @@ public class DysonTokenAuthHandler(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (valid, session, message) = await token.AuthenticateTokenAsync(tokenInfo.Token);
|
// Get client IP address
|
||||||
|
var ipAddress = Context.Connection.RemoteIpAddress?.ToString();
|
||||||
|
|
||||||
|
var (valid, session, message) = await token.AuthenticateTokenAsync(tokenInfo.Token, ipAddress);
|
||||||
if (!valid || session is null)
|
if (!valid || session is null)
|
||||||
return AuthenticateResult.Fail(message ?? "Authentication failed.");
|
return AuthenticateResult.Fail(message ?? "Authentication failed.");
|
||||||
|
|
||||||
@@ -67,7 +70,7 @@ public class DysonTokenAuthHandler(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add scopes as claims
|
// Add scopes as claims
|
||||||
session.Challenge.Scopes.ForEach(scope => claims.Add(new Claim("scope", scope)));
|
session.Challenge?.Scopes.ForEach(scope => claims.Add(new Claim("scope", scope)));
|
||||||
|
|
||||||
// Add superuser claim if applicable
|
// Add superuser claim if applicable
|
||||||
if (session.Account.IsSuperuser)
|
if (session.Account.IsSuperuser)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class AuthController(
|
|||||||
AuthService auth,
|
AuthService auth,
|
||||||
GeoIpService geo,
|
GeoIpService geo,
|
||||||
ActionLogService als,
|
ActionLogService als,
|
||||||
PusherService.PusherServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IStringLocalizer<NotificationResource> localizer
|
IStringLocalizer<NotificationResource> localizer
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
@@ -51,7 +51,11 @@ public class AuthController(
|
|||||||
.Where(e => e.Type == PunishmentType.BlockLogin || e.Type == PunishmentType.DisableAccount)
|
.Where(e => e.Type == PunishmentType.BlockLogin || e.Type == PunishmentType.DisableAccount)
|
||||||
.Where(e => e.ExpiredAt == null || now < e.ExpiredAt)
|
.Where(e => e.ExpiredAt == null || now < e.ExpiredAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (punishment is not null) return StatusCode(423, punishment);
|
if (punishment is not null)
|
||||||
|
return StatusCode(
|
||||||
|
423,
|
||||||
|
$"Your account has been suspended. Reason: {punishment.Reason}. Expired at: {punishment.ExpiredAt?.ToString() ?? "never"}"
|
||||||
|
);
|
||||||
|
|
||||||
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
|
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||||
var userAgent = HttpContext.Request.Headers.UserAgent.ToString();
|
var userAgent = HttpContext.Request.Headers.UserAgent.ToString();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Pass.Account;
|
using DysonNetwork.Pass.Account;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -52,7 +53,7 @@ public class AuthService(
|
|||||||
riskScore += 1;
|
riskScore += 1;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(lastActiveInfo?.Challenge.IpAddress) &&
|
if (!string.IsNullOrEmpty(lastActiveInfo?.Challenge?.IpAddress) &&
|
||||||
!lastActiveInfo.Challenge.IpAddress.Equals(ipAddress, StringComparison.OrdinalIgnoreCase))
|
!lastActiveInfo.Challenge.IpAddress.Equals(ipAddress, StringComparison.OrdinalIgnoreCase))
|
||||||
riskScore += 1;
|
riskScore += 1;
|
||||||
}
|
}
|
||||||
@@ -137,6 +138,7 @@ public class AuthService(
|
|||||||
|
|
||||||
var jsonOpts = new JsonSerializerOptions
|
var jsonOpts = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
|
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
};
|
};
|
||||||
@@ -211,8 +213,7 @@ public class AuthService(
|
|||||||
var session = new AuthSession
|
var session = new AuthSession
|
||||||
{
|
{
|
||||||
LastGrantedAt = now,
|
LastGrantedAt = now,
|
||||||
// Never expire server-side
|
ExpiredAt = now.Plus(Duration.FromDays(7)),
|
||||||
ExpiredAt = null,
|
|
||||||
AccountId = challenge.AccountId,
|
AccountId = challenge.AccountId,
|
||||||
ChallengeId = challenge.Id
|
ChallengeId = challenge.Id
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
using DysonNetwork.Pass.Wallet;
|
|
||||||
using DysonNetwork.Shared.Cache;
|
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth;
|
namespace DysonNetwork.Pass.Auth;
|
||||||
|
|
||||||
@@ -18,7 +14,7 @@ public class AuthServiceGrpc(
|
|||||||
ServerCallContext context
|
ServerCallContext context
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var (valid, session, message) = await token.AuthenticateTokenAsync(request.Token);
|
var (valid, session, message) = await token.AuthenticateTokenAsync(request.Token, request.IpAddress);
|
||||||
if (!valid || session is null)
|
if (!valid || session is null)
|
||||||
return new AuthenticateResponse { Valid = false, Message = message ?? "Authentication failed." };
|
return new AuthenticateResponse { Valid = false, Message = message ?? "Authentication failed." };
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
|
using DysonNetwork.Shared.GeoIp;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.Protobuf;
|
using NodaTime.Serialization.Protobuf;
|
||||||
using Point = NetTopologySuite.Geometries.Point;
|
using Point = NetTopologySuite.Geometries.Point;
|
||||||
@@ -16,11 +17,11 @@ public class AuthSession : ModelBase
|
|||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||||
|
|
||||||
// When the challenge is null, indicates the session is for an API Key
|
// When the challenge is null, indicates the session is for an API Key
|
||||||
public Guid? ChallengeId { get; set; }
|
public Guid? ChallengeId { get; set; }
|
||||||
public AuthChallenge? Challenge { get; set; } = null!;
|
public AuthChallenge? Challenge { get; set; } = null!;
|
||||||
|
|
||||||
// Indicates the session is for an OIDC connection
|
// Indicates the session is for an OIDC connection
|
||||||
public Guid? AppId { get; set; }
|
public Guid? AppId { get; set; }
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ public class AuthChallenge : ModelBase
|
|||||||
[MaxLength(128)] public string? IpAddress { get; set; }
|
[MaxLength(128)] public string? IpAddress { get; set; }
|
||||||
[MaxLength(512)] public string? UserAgent { get; set; }
|
[MaxLength(512)] public string? UserAgent { get; set; }
|
||||||
[MaxLength(1024)] public string? Nonce { get; set; }
|
[MaxLength(1024)] public string? Nonce { get; set; }
|
||||||
public Point? Location { get; set; }
|
[Column(TypeName = "jsonb")] public GeoPoint? Location { get; set; }
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||||
@@ -129,4 +130,4 @@ public class AuthClientWithChallenge : AuthClient
|
|||||||
AccountId = client.AccountId,
|
AccountId = client.AccountId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,6 @@ public class OidcProviderController(
|
|||||||
[FromForm(Name = "redirect_uri")] string? redirectUri = null,
|
[FromForm(Name = "redirect_uri")] string? redirectUri = null,
|
||||||
[FromForm] string? scope = null,
|
[FromForm] string? scope = null,
|
||||||
[FromForm] string? state = null,
|
[FromForm] string? state = null,
|
||||||
[FromForm(Name = "response_type")] string? responseType = null,
|
|
||||||
[FromForm] string? nonce = null,
|
[FromForm] string? nonce = null,
|
||||||
[FromForm(Name = "code_challenge")] string? codeChallenge = null,
|
[FromForm(Name = "code_challenge")] string? codeChallenge = null,
|
||||||
[FromForm(Name = "code_challenge_method")]
|
[FromForm(Name = "code_challenge_method")]
|
||||||
@@ -156,7 +155,7 @@ public class OidcProviderController(
|
|||||||
if (!string.IsNullOrEmpty(state)) queryParams["state"] = state;
|
if (!string.IsNullOrEmpty(state)) queryParams["state"] = state;
|
||||||
|
|
||||||
errorUri.Query = queryParams.ToString();
|
errorUri.Query = queryParams.ToString();
|
||||||
return Redirect(errorUri.Uri.ToString());
|
return Ok(new { redirectUri = errorUri.Uri.ToString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate redirect_uri if provided
|
// Validate redirect_uri if provided
|
||||||
@@ -191,7 +190,8 @@ public class OidcProviderController(
|
|||||||
scope?.Split(' ') ?? [],
|
scope?.Split(' ') ?? [],
|
||||||
codeChallenge,
|
codeChallenge,
|
||||||
codeChallengeMethod,
|
codeChallengeMethod,
|
||||||
nonce);
|
nonce
|
||||||
|
);
|
||||||
|
|
||||||
// Build the redirect URI with the authorization code
|
// Build the redirect URI with the authorization code
|
||||||
var redirectBuilder = new UriBuilder(redirectUri);
|
var redirectBuilder = new UriBuilder(redirectUri);
|
||||||
@@ -201,7 +201,7 @@ public class OidcProviderController(
|
|||||||
|
|
||||||
redirectBuilder.Query = queryParams.ToString();
|
redirectBuilder.Query = queryParams.ToString();
|
||||||
|
|
||||||
return Redirect(redirectBuilder.Uri.ToString());
|
return Ok(new { redirectUri = redirectBuilder.Uri.ToString() });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -307,7 +307,7 @@ public class OidcProviderController(
|
|||||||
HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
|
HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
|
||||||
|
|
||||||
// Get requested scopes from the token
|
// Get requested scopes from the token
|
||||||
var scopes = currentSession.Challenge.Scopes;
|
var scopes = currentSession.Challenge?.Scopes ?? [];
|
||||||
|
|
||||||
var userInfo = new Dictionary<string, object>
|
var userInfo = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ public class TokenResponse
|
|||||||
[JsonPropertyName("scope")]
|
[JsonPropertyName("scope")]
|
||||||
public string? Scope { get; set; }
|
public string? Scope { get; set; }
|
||||||
|
|
||||||
|
|
||||||
[JsonPropertyName("id_token")]
|
[JsonPropertyName("id_token")]
|
||||||
public string? IdToken { get; set; }
|
public string? IdToken { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using AccountContactType = DysonNetwork.Pass.Account.AccountContactType;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Auth.OidcProvider.Services;
|
namespace DysonNetwork.Pass.Auth.OidcProvider.Services;
|
||||||
|
|
||||||
@@ -37,12 +38,21 @@ public class OidcProviderService(
|
|||||||
return resp.App ?? null;
|
return resp.App ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AuthSession?> FindValidSessionAsync(Guid accountId, Guid clientId)
|
public async Task<AuthSession?> FindValidSessionAsync(Guid accountId, Guid clientId, bool withAccount = false)
|
||||||
{
|
{
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
return await db.AuthSessions
|
var queryable = db.AuthSessions
|
||||||
.Include(s => s.Challenge)
|
.Include(s => s.Challenge)
|
||||||
|
.AsQueryable();
|
||||||
|
if (withAccount)
|
||||||
|
queryable = queryable
|
||||||
|
.Include(s => s.Account)
|
||||||
|
.ThenInclude(a => a.Profile)
|
||||||
|
.Include(a => a.Account.Contacts)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
return await queryable
|
||||||
.Where(s => s.AccountId == accountId &&
|
.Where(s => s.AccountId == accountId &&
|
||||||
s.AppId == clientId &&
|
s.AppId == clientId &&
|
||||||
(s.ExpiredAt == null || s.ExpiredAt > now) &&
|
(s.ExpiredAt == null || s.ExpiredAt > now) &&
|
||||||
@@ -67,12 +77,12 @@ public class OidcProviderService(
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(redirectUri))
|
if (string.IsNullOrEmpty(redirectUri))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
||||||
var client = await FindClientByIdAsync(clientId);
|
var client = await FindClientByIdAsync(clientId);
|
||||||
if (client?.Status != CustomAppStatus.Production)
|
if (client?.Status != CustomAppStatus.Production)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (client?.OauthConfig?.RedirectUris == null)
|
if (client?.OauthConfig?.RedirectUris == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -114,7 +124,7 @@ public class OidcProviderService(
|
|||||||
var allowedPath = allowedUriObj.AbsolutePath.TrimEnd('/');
|
var allowedPath = allowedUriObj.AbsolutePath.TrimEnd('/');
|
||||||
var redirectPath = redirectUriObj.AbsolutePath.TrimEnd('/');
|
var redirectPath = redirectUriObj.AbsolutePath.TrimEnd('/');
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(allowedPath) ||
|
if (string.IsNullOrEmpty(allowedPath) ||
|
||||||
redirectPath.StartsWith(allowedPath, StringComparison.OrdinalIgnoreCase))
|
redirectPath.StartsWith(allowedPath, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@@ -133,6 +143,79 @@ public class OidcProviderService(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GenerateIdToken(
|
||||||
|
CustomApp client,
|
||||||
|
AuthSession session,
|
||||||
|
string? nonce = null,
|
||||||
|
IEnumerable<string>? scopes = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var clock = SystemClock.Instance;
|
||||||
|
var now = clock.GetCurrentInstant();
|
||||||
|
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(JwtRegisteredClaimNames.Iss, _options.IssuerUri),
|
||||||
|
new(JwtRegisteredClaimNames.Sub, session.AccountId.ToString()),
|
||||||
|
new(JwtRegisteredClaimNames.Aud, client.Slug),
|
||||||
|
new(JwtRegisteredClaimNames.Iat, now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
|
||||||
|
new(JwtRegisteredClaimNames.Exp,
|
||||||
|
now.Plus(Duration.FromSeconds(_options.AccessTokenLifetime.TotalSeconds)).ToUnixTimeSeconds()
|
||||||
|
.ToString(), ClaimValueTypes.Integer64),
|
||||||
|
new(JwtRegisteredClaimNames.AuthTime, session.CreatedAt.ToUnixTimeSeconds().ToString(),
|
||||||
|
ClaimValueTypes.Integer64),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add nonce if provided (required for implicit and hybrid flows)
|
||||||
|
if (!string.IsNullOrEmpty(nonce))
|
||||||
|
{
|
||||||
|
claims.Add(new Claim("nonce", nonce));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add email claim if email scope is requested
|
||||||
|
var scopesList = scopes?.ToList() ?? [];
|
||||||
|
if (scopesList.Contains("email"))
|
||||||
|
{
|
||||||
|
var contact = session.Account.Contacts.FirstOrDefault(c => c.Type == AccountContactType.Email);
|
||||||
|
if (contact is not null)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(JwtRegisteredClaimNames.Email, contact.Content));
|
||||||
|
claims.Add(new Claim("email_verified", contact.VerifiedAt is not null ? "true" : "false",
|
||||||
|
ClaimValueTypes.Boolean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add profile claims if profile scope is requested
|
||||||
|
if (scopes != null && scopesList.Contains("profile"))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(session.Account.Name))
|
||||||
|
claims.Add(new Claim("preferred_username", session.Account.Name));
|
||||||
|
if (!string.IsNullOrEmpty(session.Account.Nick))
|
||||||
|
claims.Add(new Claim("name", session.Account.Nick));
|
||||||
|
if (!string.IsNullOrEmpty(session.Account.Profile.FirstName))
|
||||||
|
claims.Add(new Claim("given_name", session.Account.Profile.FirstName));
|
||||||
|
if (!string.IsNullOrEmpty(session.Account.Profile.LastName))
|
||||||
|
claims.Add(new Claim("family_name", session.Account.Profile.LastName));
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Subject = new ClaimsIdentity(claims),
|
||||||
|
Issuer = _options.IssuerUri,
|
||||||
|
Audience = client.Id.ToString(),
|
||||||
|
Expires = now.Plus(Duration.FromSeconds(_options.AccessTokenLifetime.TotalSeconds)).ToDateTimeUtc(),
|
||||||
|
NotBefore = now.ToDateTimeUtc(),
|
||||||
|
SigningCredentials = new SigningCredentials(
|
||||||
|
new RsaSecurityKey(_options.GetRsaPrivateKey()),
|
||||||
|
SecurityAlgorithms.RsaSha256
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||||
|
return tokenHandler.WriteToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<TokenResponse> GenerateTokenResponseAsync(
|
public async Task<TokenResponse> GenerateTokenResponseAsync(
|
||||||
Guid clientId,
|
Guid clientId,
|
||||||
string? authorizationCode = null,
|
string? authorizationCode = null,
|
||||||
@@ -148,24 +231,43 @@ public class OidcProviderService(
|
|||||||
AuthSession session;
|
AuthSession session;
|
||||||
var clock = SystemClock.Instance;
|
var clock = SystemClock.Instance;
|
||||||
var now = clock.GetCurrentInstant();
|
var now = clock.GetCurrentInstant();
|
||||||
|
string? nonce = null;
|
||||||
List<string>? scopes = null;
|
List<string>? scopes = null;
|
||||||
|
|
||||||
if (authorizationCode != null)
|
if (authorizationCode != null)
|
||||||
{
|
{
|
||||||
// Authorization code flow
|
// Authorization code flow
|
||||||
var authCode = await ValidateAuthorizationCodeAsync(authorizationCode, clientId, redirectUri, codeVerifier);
|
var authCode = await ValidateAuthorizationCodeAsync(authorizationCode, clientId, redirectUri, codeVerifier);
|
||||||
if (authCode is null) throw new InvalidOperationException("Invalid authorization code");
|
if (authCode == null)
|
||||||
var account = await db.Accounts.Where(a => a.Id == authCode.AccountId).FirstOrDefaultAsync();
|
throw new InvalidOperationException("Invalid authorization code");
|
||||||
if (account is null) throw new InvalidOperationException("Account was not found");
|
|
||||||
|
// Load the session for the user
|
||||||
|
var existingSession = await FindValidSessionAsync(authCode.AccountId, clientId, withAccount: true);
|
||||||
|
|
||||||
|
if (existingSession is null)
|
||||||
|
{
|
||||||
|
var account = await db.Accounts
|
||||||
|
.Where(a => a.Id == authCode.AccountId)
|
||||||
|
.Include(a => a.Profile)
|
||||||
|
.Include(a => a.Contacts)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (account is null) throw new InvalidOperationException("Account not found");
|
||||||
|
session = await auth.CreateSessionForOidcAsync(account, clock.GetCurrentInstant(), clientId);
|
||||||
|
session.Account = account;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
session = existingSession;
|
||||||
|
}
|
||||||
|
|
||||||
session = await auth.CreateSessionForOidcAsync(account, now, clientId);
|
|
||||||
scopes = authCode.Scopes;
|
scopes = authCode.Scopes;
|
||||||
|
nonce = authCode.Nonce;
|
||||||
}
|
}
|
||||||
else if (sessionId.HasValue)
|
else if (sessionId.HasValue)
|
||||||
{
|
{
|
||||||
// Refresh token flow
|
// Refresh token flow
|
||||||
session = await FindSessionByIdAsync(sessionId.Value) ??
|
session = await FindSessionByIdAsync(sessionId.Value) ??
|
||||||
throw new InvalidOperationException("Invalid session");
|
throw new InvalidOperationException("Session not found");
|
||||||
|
|
||||||
// Verify the session is still valid
|
// Verify the session is still valid
|
||||||
if (session.ExpiredAt < now)
|
if (session.ExpiredAt < now)
|
||||||
@@ -179,13 +281,15 @@ public class OidcProviderService(
|
|||||||
var expiresIn = (int)_options.AccessTokenLifetime.TotalSeconds;
|
var expiresIn = (int)_options.AccessTokenLifetime.TotalSeconds;
|
||||||
var expiresAt = now.Plus(Duration.FromSeconds(expiresIn));
|
var expiresAt = now.Plus(Duration.FromSeconds(expiresIn));
|
||||||
|
|
||||||
// Generate an access token
|
// Generate tokens
|
||||||
var accessToken = GenerateJwtToken(client, session, expiresAt, scopes);
|
var accessToken = GenerateJwtToken(client, session, expiresAt, scopes);
|
||||||
|
var idToken = GenerateIdToken(client, session, nonce, scopes);
|
||||||
var refreshToken = GenerateRefreshToken(session);
|
var refreshToken = GenerateRefreshToken(session);
|
||||||
|
|
||||||
return new TokenResponse
|
return new TokenResponse
|
||||||
{
|
{
|
||||||
AccessToken = accessToken,
|
AccessToken = accessToken,
|
||||||
|
IdToken = idToken,
|
||||||
ExpiresIn = expiresIn,
|
ExpiresIn = expiresIn,
|
||||||
TokenType = "Bearer",
|
TokenType = "Bearer",
|
||||||
RefreshToken = refreshToken,
|
RefreshToken = refreshToken,
|
||||||
@@ -211,11 +315,10 @@ public class OidcProviderService(
|
|||||||
new Claim(JwtRegisteredClaimNames.Jti, session.Id.ToString()),
|
new Claim(JwtRegisteredClaimNames.Jti, session.Id.ToString()),
|
||||||
new Claim(JwtRegisteredClaimNames.Iat, now.ToUnixTimeSeconds().ToString(),
|
new Claim(JwtRegisteredClaimNames.Iat, now.ToUnixTimeSeconds().ToString(),
|
||||||
ClaimValueTypes.Integer64),
|
ClaimValueTypes.Integer64),
|
||||||
new Claim("client_id", client.Id)
|
|
||||||
]),
|
]),
|
||||||
Expires = expiresAt.ToDateTimeUtc(),
|
Expires = expiresAt.ToDateTimeUtc(),
|
||||||
Issuer = _options.IssuerUri,
|
Issuer = _options.IssuerUri,
|
||||||
Audience = client.Id
|
Audience = client.Slug
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to use RSA signing if keys are available, fall back to HMAC
|
// Try to use RSA signing if keys are available, fall back to HMAC
|
||||||
@@ -281,51 +384,6 @@ public class OidcProviderService(
|
|||||||
return Convert.ToBase64String(session.Id.ToByteArray());
|
return Convert.ToBase64String(session.Id.ToByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool VerifyHashedSecret(string secret, string hashedSecret)
|
|
||||||
{
|
|
||||||
// In a real implementation, you'd use a proper password hashing algorithm like PBKDF2, bcrypt, or Argon2
|
|
||||||
// For now, we'll do a simple comparison, but you should replace this with proper hashing
|
|
||||||
return string.Equals(secret, hashedSecret, StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> GenerateAuthorizationCodeForReuseSessionAsync(
|
|
||||||
AuthSession session,
|
|
||||||
Guid clientId,
|
|
||||||
string redirectUri,
|
|
||||||
IEnumerable<string> scopes,
|
|
||||||
string? codeChallenge = null,
|
|
||||||
string? codeChallengeMethod = null,
|
|
||||||
string? nonce = null)
|
|
||||||
{
|
|
||||||
var clock = SystemClock.Instance;
|
|
||||||
var now = clock.GetCurrentInstant();
|
|
||||||
var code = Guid.NewGuid().ToString("N");
|
|
||||||
|
|
||||||
// Update the session's last activity time
|
|
||||||
await db.AuthSessions.Where(s => s.Id == session.Id)
|
|
||||||
.ExecuteUpdateAsync(s => s.SetProperty(s => s.LastGrantedAt, now));
|
|
||||||
|
|
||||||
// Create the authorization code info
|
|
||||||
var authCodeInfo = new AuthorizationCodeInfo
|
|
||||||
{
|
|
||||||
ClientId = clientId,
|
|
||||||
AccountId = session.AccountId,
|
|
||||||
RedirectUri = redirectUri,
|
|
||||||
Scopes = scopes.ToList(),
|
|
||||||
CodeChallenge = codeChallenge,
|
|
||||||
CodeChallengeMethod = codeChallengeMethod,
|
|
||||||
Nonce = nonce,
|
|
||||||
CreatedAt = now
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store the code with its metadata in the cache
|
|
||||||
var cacheKey = $"auth:code:{code}";
|
|
||||||
await cache.SetAsync(cacheKey, authCodeInfo, _options.AuthorizationCodeLifetime);
|
|
||||||
|
|
||||||
logger.LogInformation("Generated authorization code for client {ClientId} and user {UserId}", clientId, session.AccountId);
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> GenerateAuthorizationCodeAsync(
|
public async Task<string> GenerateAuthorizationCodeAsync(
|
||||||
Guid clientId,
|
Guid clientId,
|
||||||
Guid userId,
|
Guid userId,
|
||||||
@@ -355,7 +413,7 @@ public class OidcProviderService(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Store the code with its metadata in the cache
|
// Store the code with its metadata in the cache
|
||||||
var cacheKey = $"auth:code:{code}";
|
var cacheKey = $"auth:oidc-code:{code}";
|
||||||
await cache.SetAsync(cacheKey, authCodeInfo, _options.AuthorizationCodeLifetime);
|
await cache.SetAsync(cacheKey, authCodeInfo, _options.AuthorizationCodeLifetime);
|
||||||
|
|
||||||
logger.LogInformation("Generated authorization code for client {ClientId} and user {UserId}", clientId, userId);
|
logger.LogInformation("Generated authorization code for client {ClientId} and user {UserId}", clientId, userId);
|
||||||
@@ -369,7 +427,7 @@ public class OidcProviderService(
|
|||||||
string? codeVerifier = null
|
string? codeVerifier = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var cacheKey = $"auth:code:{code}";
|
var cacheKey = $"auth:oidc-code:{code}";
|
||||||
var (found, authCode) = await cache.GetAsyncWithStatus<AuthorizationCodeInfo>(cacheKey);
|
var (found, authCode) = await cache.GetAsyncWithStatus<AuthorizationCodeInfo>(cacheKey);
|
||||||
|
|
||||||
if (!found || authCode == null)
|
if (!found || authCode == null)
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ public class ConnectionController(
|
|||||||
|
|
||||||
var loginSession = await auth.CreateSessionForOidcAsync(account, clock.GetCurrentInstant());
|
var loginSession = await auth.CreateSessionForOidcAsync(account, clock.GetCurrentInstant());
|
||||||
var loginToken = auth.CreateToken(loginSession);
|
var loginToken = auth.CreateToken(loginSession);
|
||||||
return Redirect($"/auth/token?token={loginToken}");
|
return Redirect($"/auth/callback?token={loginToken}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<OidcCallbackData> ExtractCallbackData(HttpRequest request)
|
private static async Task<OidcCallbackData> ExtractCallbackData(HttpRequest request)
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ public class OidcState
|
|||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(this, new JsonSerializerOptions
|
return JsonSerializer.Serialize(this, new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using DysonNetwork.Pass.Wallet;
|
using DysonNetwork.Pass.Wallet;
|
||||||
@@ -22,8 +23,9 @@ public class TokenAuthService(
|
|||||||
/// then cache and return.
|
/// then cache and return.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="token">Incoming token string</param>
|
/// <param name="token">Incoming token string</param>
|
||||||
|
/// <param name="ipAddress">Client IP address, for logging purposes</param>
|
||||||
/// <returns>(Valid, Session, Message)</returns>
|
/// <returns>(Valid, Session, Message)</returns>
|
||||||
public async Task<(bool Valid, AuthSession? Session, string? Message)> AuthenticateTokenAsync(string token)
|
public async Task<(bool Valid, AuthSession? Session, string? Message)> AuthenticateTokenAsync(string token, string? ipAddress = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -32,6 +34,11 @@ public class TokenAuthService(
|
|||||||
logger.LogWarning("AuthenticateTokenAsync: no token provided");
|
logger.LogWarning("AuthenticateTokenAsync: no token provided");
|
||||||
return (false, null, "No token provided.");
|
return (false, null, "No token provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(ipAddress))
|
||||||
|
{
|
||||||
|
logger.LogDebug("AuthenticateTokenAsync: client IP: {IpAddress}", ipAddress);
|
||||||
|
}
|
||||||
|
|
||||||
// token fingerprint for correlation
|
// token fingerprint for correlation
|
||||||
var tokenHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));
|
var tokenHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));
|
||||||
@@ -70,7 +77,7 @@ public class TokenAuthService(
|
|||||||
"AuthenticateTokenAsync: success via cache (sessionId={SessionId}, accountId={AccountId}, scopes={ScopeCount}, expiresAt={ExpiresAt})",
|
"AuthenticateTokenAsync: success via cache (sessionId={SessionId}, accountId={AccountId}, scopes={ScopeCount}, expiresAt={ExpiresAt})",
|
||||||
sessionId,
|
sessionId,
|
||||||
session.AccountId,
|
session.AccountId,
|
||||||
session.Challenge.Scopes.Count,
|
session.Challenge?.Scopes.Count,
|
||||||
session.ExpiredAt
|
session.ExpiredAt
|
||||||
);
|
);
|
||||||
return (true, session, null);
|
return (true, session, null);
|
||||||
@@ -103,11 +110,11 @@ public class TokenAuthService(
|
|||||||
"AuthenticateTokenAsync: DB session loaded (sessionId={SessionId}, accountId={AccountId}, clientId={ClientId}, appId={AppId}, scopes={ScopeCount}, ip={Ip}, uaLen={UaLen})",
|
"AuthenticateTokenAsync: DB session loaded (sessionId={SessionId}, accountId={AccountId}, clientId={ClientId}, appId={AppId}, scopes={ScopeCount}, ip={Ip}, uaLen={UaLen})",
|
||||||
sessionId,
|
sessionId,
|
||||||
session.AccountId,
|
session.AccountId,
|
||||||
session.Challenge.ClientId,
|
session.Challenge?.ClientId,
|
||||||
session.AppId,
|
session.AppId,
|
||||||
session.Challenge.Scopes.Count,
|
session.Challenge?.Scopes.Count,
|
||||||
session.Challenge.IpAddress,
|
session.Challenge?.IpAddress,
|
||||||
(session.Challenge.UserAgent ?? string.Empty).Length
|
(session.Challenge?.UserAgent ?? string.Empty).Length
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.LogDebug("AuthenticateTokenAsync: enriching account with subscription (accountId={AccountId})", session.AccountId);
|
logger.LogDebug("AuthenticateTokenAsync: enriching account with subscription (accountId={AccountId})", session.AccountId);
|
||||||
@@ -136,7 +143,7 @@ public class TokenAuthService(
|
|||||||
"AuthenticateTokenAsync: success via DB (sessionId={SessionId}, accountId={AccountId}, clientId={ClientId})",
|
"AuthenticateTokenAsync: success via DB (sessionId={SessionId}, accountId={AccountId}, clientId={ClientId})",
|
||||||
sessionId,
|
sessionId,
|
||||||
session.AccountId,
|
session.AccountId,
|
||||||
session.Challenge.ClientId
|
session.Challenge?.ClientId
|
||||||
);
|
);
|
||||||
return (true, session, null);
|
return (true, session, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,15 +169,15 @@ function handleDeny() {
|
|||||||
error_description: 'The user denied the authorization request',
|
error_description: 'The user denied the authorization request',
|
||||||
state: state,
|
state: state,
|
||||||
})
|
})
|
||||||
window.location.href = `${redirectUri}?${params}`
|
window.open(`${redirectUri}?${params}`, "_self")
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTerms() {
|
function openTerms() {
|
||||||
window.open(clientInfo.value?.terms_of_service_uri || 'https://example.com/terms', '_blank')
|
window.open(clientInfo.value?.terms_of_service_uri || '#', "_blank")
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPrivacy() {
|
function openPrivacy() {
|
||||||
window.open(clientInfo.value?.privacy_policy_uri || 'https://example.com/privacy', '_blank')
|
window.open(clientInfo.value?.privacy_policy_uri || '#', "_blank")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="NATS.Client.Core" Version="2.6.6" />
|
<PackageReference Include="Nager.Holiday" Version="1.0.1" />
|
||||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
|
<PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
@@ -24,7 +24,6 @@
|
|||||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/>
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4"/>
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4"/>
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4"/>
|
||||||
<PackageReference Include="OpenGraph-Net" Version="4.0.1" />
|
<PackageReference Include="OpenGraph-Net" Version="4.0.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
|
||||||
@@ -50,6 +49,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" />
|
||||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj"/>
|
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
using dotnet_etcd;
|
|
||||||
using dotnet_etcd.interfaces;
|
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Email;
|
namespace DysonNetwork.Pass.Email;
|
||||||
|
|
||||||
public class EmailService(
|
public class EmailService(
|
||||||
PusherService.PusherServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
RazorViewRenderer viewRenderer,
|
RazorViewRenderer viewRenderer,
|
||||||
ILogger<EmailService> logger
|
ILogger<EmailService> logger
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class LastActiveFlushHandler(IServiceProvider srp, ILogger<LastActiveFlus
|
|||||||
public async Task FlushAsync(IReadOnlyList<LastActiveInfo> items)
|
public async Task FlushAsync(IReadOnlyList<LastActiveInfo> items)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Flushing {Count} LastActiveInfo items...", items.Count);
|
logger.LogInformation("Flushing {Count} LastActiveInfo items...", items.Count);
|
||||||
|
|
||||||
using var scope = srp.CreateScope();
|
using var scope = srp.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||||
|
|
||||||
@@ -38,13 +38,22 @@ public class LastActiveFlushHandler(IServiceProvider srp, ILogger<LastActiveFlus
|
|||||||
.ToDictionary(g => g.Key, g => g.Last().SeenAt);
|
.ToDictionary(g => g.Key, g => g.Last().SeenAt);
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
var updatingSessions = sessionMap.Select(x => x.Key).ToList();
|
var updatingSessions = sessionMap.Select(x => x.Key).ToList();
|
||||||
var sessionUpdates = await db.AuthSessions
|
var sessionUpdates = await db.AuthSessions
|
||||||
.Where(s => updatingSessions.Contains(s.Id))
|
.Where(s => updatingSessions.Contains(s.Id))
|
||||||
.ExecuteUpdateAsync(s => s.SetProperty(x => x.LastGrantedAt, now));
|
.ExecuteUpdateAsync(s =>
|
||||||
|
s.SetProperty(x => x.LastGrantedAt, now)
|
||||||
|
);
|
||||||
logger.LogInformation("Updated {Count} auth sessions according to LastActiveInfo", sessionUpdates);
|
logger.LogInformation("Updated {Count} auth sessions according to LastActiveInfo", sessionUpdates);
|
||||||
|
var newExpiration = now.Plus(Duration.FromDays(7));
|
||||||
|
var keepAliveSessionUpdates = await db.AuthSessions
|
||||||
|
.Where(s => updatingSessions.Contains(s.Id) && s.ExpiredAt != null)
|
||||||
|
.ExecuteUpdateAsync(s =>
|
||||||
|
s.SetProperty(x => x.ExpiredAt, newExpiration)
|
||||||
|
);
|
||||||
|
logger.LogInformation("Updated {Count} auth sessions' duration according to LastActiveInfo", sessionUpdates);
|
||||||
|
|
||||||
var updatingAccounts = accountMap.Select(x => x.Key).ToList();
|
var updatingAccounts = accountMap.Select(x => x.Key).ToList();
|
||||||
var profileUpdates = await db.AccountProfiles
|
var profileUpdates = await db.AccountProfiles
|
||||||
.Where(a => updatingAccounts.Contains(a.AccountId))
|
.Where(a => updatingAccounts.Contains(a.AccountId))
|
||||||
@@ -53,7 +62,8 @@ public class LastActiveFlushHandler(IServiceProvider srp, ILogger<LastActiveFlus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LastActiveFlushJob(FlushBufferService fbs, LastActiveFlushHandler hdl, ILogger<LastActiveFlushJob> logger) : IJob
|
public class LastActiveFlushJob(FlushBufferService fbs, LastActiveFlushHandler hdl, ILogger<LastActiveFlushJob> logger)
|
||||||
|
: IJob
|
||||||
{
|
{
|
||||||
public async Task Execute(IJobExecutionContext context)
|
public async Task Execute(IJobExecutionContext context)
|
||||||
{
|
{
|
||||||
@@ -62,7 +72,8 @@ public class LastActiveFlushJob(FlushBufferService fbs, LastActiveFlushHandler h
|
|||||||
logger.LogInformation("Running LastActiveInfo flush job...");
|
logger.LogInformation("Running LastActiveInfo flush job...");
|
||||||
await fbs.FlushAsync(hdl);
|
await fbs.FlushAsync(hdl);
|
||||||
logger.LogInformation("Completed LastActiveInfo flush job...");
|
logger.LogInformation("Completed LastActiveInfo flush job...");
|
||||||
} catch (Exception ex)
|
}
|
||||||
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error running LastActiveInfo job...");
|
logger.LogError(ex, "Error running LastActiveInfo job...");
|
||||||
}
|
}
|
||||||
|
|||||||
39
DysonNetwork.Pass/IpCheckController.cs
Normal file
39
DysonNetwork.Pass/IpCheckController.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/api/ip-check")]
|
||||||
|
public class IpCheckController : ControllerBase
|
||||||
|
{
|
||||||
|
public class IpCheckResponse
|
||||||
|
{
|
||||||
|
public string? RemoteIp { get; set; }
|
||||||
|
public string? XForwardedFor { get; set; }
|
||||||
|
public string? XForwardedProto { get; set; }
|
||||||
|
public string? XForwardedHost { get; set; }
|
||||||
|
public string? XRealIp { get; set; }
|
||||||
|
public string? Headers { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public ActionResult<IpCheckResponse> GetIpCheck()
|
||||||
|
{
|
||||||
|
var ip = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||||
|
|
||||||
|
var xForwardedFor = Request.Headers["X-Forwarded-For"].FirstOrDefault();
|
||||||
|
var xForwardedProto = Request.Headers["X-Forwarded-Proto"].FirstOrDefault();
|
||||||
|
var xForwardedHost = Request.Headers["X-Forwarded-Host"].FirstOrDefault();
|
||||||
|
var realIp = Request.Headers["X-Real-IP"].FirstOrDefault();
|
||||||
|
|
||||||
|
return Ok(new IpCheckResponse
|
||||||
|
{
|
||||||
|
RemoteIp = ip,
|
||||||
|
XForwardedFor = xForwardedFor,
|
||||||
|
XForwardedProto = xForwardedProto,
|
||||||
|
XForwardedHost = xForwardedHost,
|
||||||
|
XRealIp = realIp,
|
||||||
|
Headers = string.Join('\n', Request.Headers.Select(h => $"{h.Key}: {h.Value}")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
2021
DysonNetwork.Pass/Migrations/20250904144723_AddOrderProductIdentifier.Designer.cs
generated
Normal file
2021
DysonNetwork.Pass/Migrations/20250904144723_AddOrderProductIdentifier.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddOrderProductIdentifier : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "product_identifier",
|
||||||
|
table: "payment_orders",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "product_identifier",
|
||||||
|
table: "payment_orders");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2027
DysonNetwork.Pass/Migrations/20250906174610_AddAccountRegion.Designer.cs
generated
Normal file
2027
DysonNetwork.Pass/Migrations/20250906174610_AddAccountRegion.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAccountRegion : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "region",
|
||||||
|
table: "accounts",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "region",
|
||||||
|
table: "accounts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2027
DysonNetwork.Pass/Migrations/20250907065433_RefactorGeoIpPoint.Designer.cs
generated
Normal file
2027
DysonNetwork.Pass/Migrations/20250907065433_RefactorGeoIpPoint.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
|||||||
|
using DysonNetwork.Shared.GeoIp;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NetTopologySuite.Geometries;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RefactorGeoIpPoint : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("UPDATE auth_challenges SET location = NULL;");
|
||||||
|
migrationBuilder.Sql("UPDATE action_logs SET location = NULL;");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "location",
|
||||||
|
table: "auth_challenges");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<GeoPoint>(
|
||||||
|
name: "location",
|
||||||
|
table: "auth_challenges",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "location",
|
||||||
|
table: "action_logs");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<GeoPoint>(
|
||||||
|
name: "location",
|
||||||
|
table: "action_logs",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "location",
|
||||||
|
table: "auth_challenges");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Point>(
|
||||||
|
name: "location",
|
||||||
|
table: "auth_challenges",
|
||||||
|
type: "geometry",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "location",
|
||||||
|
table: "action_logs");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Point>(
|
||||||
|
name: "location",
|
||||||
|
table: "action_logs",
|
||||||
|
type: "geometry",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2026
DysonNetwork.Pass/Migrations/20250907065933_RemoveNetTopo.Designer.cs
generated
Normal file
2026
DysonNetwork.Pass/Migrations/20250907065933_RemoveNetTopo.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
DysonNetwork.Pass/Migrations/20250907065933_RemoveNetTopo.cs
Normal file
24
DysonNetwork.Pass/Migrations/20250907065933_RemoveNetTopo.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RemoveNetTopo : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.OldAnnotation("Npgsql:PostgresExtension:postgis", ",,");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2035
DysonNetwork.Pass/Migrations/20250908151924_AddAutomatedStatus.Designer.cs
generated
Normal file
2035
DysonNetwork.Pass/Migrations/20250908151924_AddAutomatedStatus.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAutomatedStatus : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "app_identifier",
|
||||||
|
table: "account_statuses",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "is_automated",
|
||||||
|
table: "account_statuses",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "app_identifier",
|
||||||
|
table: "account_statuses");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "is_automated",
|
||||||
|
table: "account_statuses");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,10 @@ using DysonNetwork.Pass;
|
|||||||
using DysonNetwork.Pass.Account;
|
using DysonNetwork.Pass.Account;
|
||||||
using DysonNetwork.Pass.Wallet;
|
using DysonNetwork.Pass.Wallet;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
|
using DysonNetwork.Shared.GeoIp;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using NetTopologySuite.Geometries;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
@@ -27,7 +27,6 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
.HasAnnotation("ProductVersion", "9.0.7")
|
.HasAnnotation("ProductVersion", "9.0.7")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Pass.Account.AbuseReport", b =>
|
modelBuilder.Entity("DysonNetwork.Pass.Account.AbuseReport", b =>
|
||||||
@@ -132,6 +131,12 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
.HasColumnType("character varying(256)")
|
.HasColumnType("character varying(256)")
|
||||||
.HasColumnName("nick");
|
.HasColumnName("nick");
|
||||||
|
|
||||||
|
b.Property<string>("Region")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasColumnName("region");
|
||||||
|
|
||||||
b.Property<Instant>("UpdatedAt")
|
b.Property<Instant>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("updated_at");
|
.HasColumnName("updated_at");
|
||||||
@@ -519,8 +524,8 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
.HasColumnType("character varying(128)")
|
.HasColumnType("character varying(128)")
|
||||||
.HasColumnName("ip_address");
|
.HasColumnName("ip_address");
|
||||||
|
|
||||||
b.Property<Point>("Location")
|
b.Property<GeoPoint>("Location")
|
||||||
.HasColumnType("geometry")
|
.HasColumnType("jsonb")
|
||||||
.HasColumnName("location");
|
.HasColumnName("location");
|
||||||
|
|
||||||
b.Property<Dictionary<string, object>>("Meta")
|
b.Property<Dictionary<string, object>>("Meta")
|
||||||
@@ -762,6 +767,11 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("account_id");
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<string>("AppIdentifier")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("app_identifier");
|
||||||
|
|
||||||
b.Property<int>("Attitude")
|
b.Property<int>("Attitude")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasColumnName("attitude");
|
.HasColumnName("attitude");
|
||||||
@@ -778,6 +788,10 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("deleted_at");
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAutomated")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_automated");
|
||||||
|
|
||||||
b.Property<bool>("IsInvisible")
|
b.Property<bool>("IsInvisible")
|
||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasColumnName("is_invisible");
|
.HasColumnName("is_invisible");
|
||||||
@@ -895,8 +909,8 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
.HasColumnType("character varying(128)")
|
.HasColumnType("character varying(128)")
|
||||||
.HasColumnName("ip_address");
|
.HasColumnName("ip_address");
|
||||||
|
|
||||||
b.Property<Point>("Location")
|
b.Property<GeoPoint>("Location")
|
||||||
.HasColumnType("geometry")
|
.HasColumnType("jsonb")
|
||||||
.HasColumnName("location");
|
.HasColumnName("location");
|
||||||
|
|
||||||
b.Property<string>("Nonce")
|
b.Property<string>("Nonce")
|
||||||
@@ -1381,6 +1395,11 @@ namespace DysonNetwork.Pass.Migrations
|
|||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("payee_wallet_id");
|
.HasColumnName("payee_wallet_id");
|
||||||
|
|
||||||
|
b.Property<string>("ProductIdentifier")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("product_identifier");
|
||||||
|
|
||||||
b.Property<string>("Remarks")
|
b.Property<string>("Remarks")
|
||||||
.HasMaxLength(4096)
|
.HasMaxLength(4096)
|
||||||
.HasColumnType("character varying(4096)")
|
.HasColumnType("character varying(4096)")
|
||||||
|
|||||||
@@ -4,25 +4,21 @@ using DysonNetwork.Pass.Startup;
|
|||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
using DysonNetwork.Shared.PageData;
|
using DysonNetwork.Shared.PageData;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using DysonNetwork.Shared.Stream;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.AddServiceDefaults();
|
||||||
|
|
||||||
// Configure Kestrel and server options
|
// Configure Kestrel and server options
|
||||||
builder.ConfigureAppKestrel(builder.Configuration);
|
builder.ConfigureAppKestrel(builder.Configuration);
|
||||||
|
|
||||||
// Add metrics and telemetry
|
|
||||||
builder.Services.AddAppMetrics();
|
|
||||||
|
|
||||||
// Add application services
|
// Add application services
|
||||||
builder.Services.AddRegistryService(builder.Configuration);
|
|
||||||
builder.Services.AddStreamConnection(builder.Configuration);
|
|
||||||
builder.Services.AddAppServices(builder.Configuration);
|
builder.Services.AddAppServices(builder.Configuration);
|
||||||
builder.Services.AddAppRateLimiting();
|
builder.Services.AddAppRateLimiting();
|
||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
builder.Services.AddAppSwagger();
|
builder.Services.AddAppSwagger();
|
||||||
builder.Services.AddPusherService();
|
builder.Services.AddRingService();
|
||||||
builder.Services.AddDriveService();
|
builder.Services.AddDriveService();
|
||||||
builder.Services.AddDevelopService();
|
builder.Services.AddDevelopService();
|
||||||
|
|
||||||
@@ -41,6 +37,8 @@ builder.Services.AddTransient<IPageDataProvider, AccountPageData>();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.MapDefaultEndpoints();
|
||||||
|
|
||||||
// Run database migrations
|
// Run database migrations
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
@@ -51,8 +49,6 @@ using (var scope = app.Services.CreateScope())
|
|||||||
// Configure application middleware pipeline
|
// Configure application middleware pipeline
|
||||||
app.ConfigureAppMiddleware(builder.Configuration, builder.Environment.ContentRootPath);
|
app.ConfigureAppMiddleware(builder.Configuration, builder.Environment.ContentRootPath);
|
||||||
|
|
||||||
app.MapGatewayProxy();
|
|
||||||
|
|
||||||
app.MapPages(Path.Combine(builder.Environment.WebRootPath, "dist", "index.html"));
|
app.MapPages(Path.Combine(builder.Environment.WebRootPath, "dist", "index.html"));
|
||||||
|
|
||||||
// Configure gRPC
|
// Configure gRPC
|
||||||
|
|||||||
@@ -170,5 +170,47 @@ namespace DysonNetwork.Sphere.Resources.Localization {
|
|||||||
return ResourceManager.GetString("NewLoginBody", resourceCulture);
|
return ResourceManager.GetString("NewLoginBody", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static string FriendRequestTitle {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("FriendRequestTitle", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string FriendRequestBody {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("FriendRequestBody", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string OrderReceivedTitle {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("OrderReceivedTitle", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string OrderReceivedBody {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("OrderReceivedBody", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string TransactionNewTitle {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TransactionNewTitle", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string TransactionNewBodyPlus {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TransactionNewBodyPlus", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string TransactionNewBodyMinus {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("TransactionNewBodyMinus", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<value>Order {0} recipent</value>
|
<value>Order {0} recipent</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OrderPaidBody" xml:space="preserve">
|
<data name="OrderPaidBody" xml:space="preserve">
|
||||||
<value>{0} {1} was removed from your wallet to pay {2}</value>
|
<value>Paid order {2} with {0} {1}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="NewLoginTitle" xml:space="preserve">
|
<data name="NewLoginTitle" xml:space="preserve">
|
||||||
<value>New login detected</value>
|
<value>New login detected</value>
|
||||||
@@ -92,4 +92,19 @@
|
|||||||
<data name="FriendRequestBody" xml:space="preserve">
|
<data name="FriendRequestBody" xml:space="preserve">
|
||||||
<value>You can go to relationships page and decide accept their request or not.</value>
|
<value>You can go to relationships page and decide accept their request or not.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="OrderReceivedTitle" xml:space="preserve">
|
||||||
|
<value>Order {0} recipent</value>
|
||||||
|
</data>
|
||||||
|
<data name="OrderReceivedBody" xml:space="preserve">
|
||||||
|
<value>Received {2} payment of {0} {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TransactionNewTitle" xml:space="preserve">
|
||||||
|
<value>Transaction {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TransactionNewBodyPlus" xml:space="preserve">
|
||||||
|
<value>{0} {1} added to your wallet</value>
|
||||||
|
</data>
|
||||||
|
<data name="TransactionNewBodyMinus" xml:space="preserve">
|
||||||
|
<value>{0} {1} removed from your wallet</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -67,10 +67,10 @@
|
|||||||
<value>感谢你支持 Solar Network 的开发!你的 {0} 天 {1} 订阅刚刚开始,接下来来探索新解锁的新功能吧!</value>
|
<value>感谢你支持 Solar Network 的开发!你的 {0} 天 {1} 订阅刚刚开始,接下来来探索新解锁的新功能吧!</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OrderPaidTitle" xml:space="preserve">
|
<data name="OrderPaidTitle" xml:space="preserve">
|
||||||
<value>订单回执 {0}</value>
|
<value>订单收据 {0}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="OrderPaidBody" xml:space="preserve">
|
<data name="OrderPaidBody" xml:space="preserve">
|
||||||
<value>{0} {1} 已从你的帐户中扣除来支付 {2}</value>
|
<value>已支付订单 {2} 的 {0} {1}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="NewLoginTitle" xml:space="preserve">
|
<data name="NewLoginTitle" xml:space="preserve">
|
||||||
<value>检测到新登陆</value>
|
<value>检测到新登陆</value>
|
||||||
@@ -84,4 +84,19 @@
|
|||||||
<data name="FriendRequestBody" xml:space="preserve">
|
<data name="FriendRequestBody" xml:space="preserve">
|
||||||
<value>您可以前往人际关系页面来决定时候要接受他们的邀请。</value>
|
<value>您可以前往人际关系页面来决定时候要接受他们的邀请。</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="OrderReceivedTitle" xml:space="preserve">
|
||||||
|
<value>订单收据 {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OrderReceivedBody" xml:space="preserve">
|
||||||
|
<value>收到订单 {2} 支付的 {0} {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TransactionNewTitle" xml:space="preserve">
|
||||||
|
<value>交易 {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="TransactionNewBodyPlus" xml:space="preserve">
|
||||||
|
<value>{0} {1} 添加到了您的钱包</value>
|
||||||
|
</data>
|
||||||
|
<data name="TransactionNewBodyMinus" xml:space="preserve">
|
||||||
|
<value>{0} {1} 从您的钱包移除</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -4,6 +4,8 @@ using DysonNetwork.Pass.Auth;
|
|||||||
using DysonNetwork.Pass.Credit;
|
using DysonNetwork.Pass.Credit;
|
||||||
using DysonNetwork.Pass.Leveling;
|
using DysonNetwork.Pass.Leveling;
|
||||||
using DysonNetwork.Pass.Permission;
|
using DysonNetwork.Pass.Permission;
|
||||||
|
using DysonNetwork.Pass.Wallet;
|
||||||
|
using DysonNetwork.Shared.Http;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Prometheus;
|
using Prometheus;
|
||||||
@@ -22,7 +24,7 @@ public static class ApplicationConfiguration
|
|||||||
|
|
||||||
app.UseRequestLocalization();
|
app.UseRequestLocalization();
|
||||||
|
|
||||||
ConfigureForwardedHeaders(app, configuration);
|
app.ConfigureForwardedHeaders(configuration);
|
||||||
|
|
||||||
app.UseCors(opts =>
|
app.UseCors(opts =>
|
||||||
opts.SetIsOriginAllowed(_ => true)
|
opts.SetIsOriginAllowed(_ => true)
|
||||||
@@ -50,28 +52,6 @@ public static class ApplicationConfiguration
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureForwardedHeaders(WebApplication app, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
var knownProxiesSection = configuration.GetSection("KnownProxies");
|
|
||||||
var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
|
|
||||||
|
|
||||||
if (knownProxiesSection.Exists())
|
|
||||||
{
|
|
||||||
var proxyAddresses = knownProxiesSection.Get<string[]>();
|
|
||||||
if (proxyAddresses != null)
|
|
||||||
foreach (var proxy in proxyAddresses)
|
|
||||||
if (IPAddress.TryParse(proxy, out var ipAddress))
|
|
||||||
forwardedHeadersOptions.KnownProxies.Add(ipAddress);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.Any);
|
|
||||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.IPv6Any);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseForwardedHeaders(forwardedHeadersOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebApplication ConfigureGrpcServices(this WebApplication app)
|
public static WebApplication ConfigureGrpcServices(this WebApplication app)
|
||||||
{
|
{
|
||||||
app.MapGrpcService<AccountServiceGrpc>();
|
app.MapGrpcService<AccountServiceGrpc>();
|
||||||
@@ -81,6 +61,8 @@ public static class ApplicationConfiguration
|
|||||||
app.MapGrpcService<SocialCreditServiceGrpc>();
|
app.MapGrpcService<SocialCreditServiceGrpc>();
|
||||||
app.MapGrpcService<ExperienceServiceGrpc>();
|
app.MapGrpcService<ExperienceServiceGrpc>();
|
||||||
app.MapGrpcService<BotAccountReceiverGrpc>();
|
app.MapGrpcService<BotAccountReceiverGrpc>();
|
||||||
|
app.MapGrpcService<WalletServiceGrpc>();
|
||||||
|
app.MapGrpcService<PaymentServiceGrpc>();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
73
DysonNetwork.Pass/Startup/BroadcastEventHandler.cs
Normal file
73
DysonNetwork.Pass/Startup/BroadcastEventHandler.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Pass.Wallet;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using DysonNetwork.Shared.Stream;
|
||||||
|
using NATS.Client.Core;
|
||||||
|
using NATS.Client.JetStream.Models;
|
||||||
|
using NATS.Net;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Pass.Startup;
|
||||||
|
|
||||||
|
public class BroadcastEventHandler(
|
||||||
|
INatsConnection nats,
|
||||||
|
ILogger<BroadcastEventHandler> logger,
|
||||||
|
IServiceProvider serviceProvider
|
||||||
|
) : BackgroundService
|
||||||
|
{
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
var js = nats.CreateJetStreamContext();
|
||||||
|
|
||||||
|
await js.EnsureStreamCreated("payment_events", [PaymentOrderEventBase.Type]);
|
||||||
|
|
||||||
|
var consumer = await js.CreateOrUpdateConsumerAsync("payment_events",
|
||||||
|
new ConsumerConfig("pass_payment_handler"),
|
||||||
|
cancellationToken: stoppingToken);
|
||||||
|
|
||||||
|
await foreach (var msg in consumer.ConsumeAsync<byte[]>(cancellationToken: stoppingToken))
|
||||||
|
{
|
||||||
|
PaymentOrderEvent? evt = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
evt = JsonSerializer.Deserialize<PaymentOrderEvent>(msg.Data, GrpcTypeHelper.SerializerOptions);
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Received order event: {ProductIdentifier} {OrderId}",
|
||||||
|
evt?.ProductIdentifier,
|
||||||
|
evt?.OrderId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (evt?.ProductIdentifier is null ||
|
||||||
|
!evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
logger.LogInformation("Handling stellar program order: {OrderId}", evt.OrderId);
|
||||||
|
|
||||||
|
await using var scope = serviceProvider.CreateAsyncScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||||
|
var subscriptions = scope.ServiceProvider.GetRequiredService<SubscriptionService>();
|
||||||
|
|
||||||
|
var order = await db.PaymentOrders.FindAsync(
|
||||||
|
[evt.OrderId],
|
||||||
|
cancellationToken: stoppingToken
|
||||||
|
);
|
||||||
|
if (order is null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId);
|
||||||
|
await msg.NakAsync(cancellationToken: stoppingToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await subscriptions.HandleSubscriptionOrder(order);
|
||||||
|
|
||||||
|
logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId);
|
||||||
|
await msg.AckAsync(cancellationToken: stoppingToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error processing payment order event for order {OrderId}. Redelivering.", evt?.OrderId);
|
||||||
|
await msg.NakAsync(cancellationToken: stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using OpenTelemetry.Metrics;
|
|
||||||
using OpenTelemetry.Trace;
|
|
||||||
using Prometheus;
|
|
||||||
using Prometheus.SystemMetrics;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Startup;
|
|
||||||
|
|
||||||
public static class MetricsConfiguration
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddAppMetrics(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
// Prometheus
|
|
||||||
services.UseHttpClientMetrics();
|
|
||||||
services.AddHealthChecks();
|
|
||||||
services.AddSystemMetrics();
|
|
||||||
services.AddPrometheusEntityFrameworkMetrics();
|
|
||||||
services.AddPrometheusAspNetCoreMetrics();
|
|
||||||
services.AddPrometheusHttpClientMetrics();
|
|
||||||
|
|
||||||
// OpenTelemetry
|
|
||||||
services.AddOpenTelemetry()
|
|
||||||
.WithTracing(tracing =>
|
|
||||||
{
|
|
||||||
tracing
|
|
||||||
.AddAspNetCoreInstrumentation()
|
|
||||||
.AddHttpClientInstrumentation()
|
|
||||||
.AddOtlpExporter();
|
|
||||||
})
|
|
||||||
.WithMetrics(metrics =>
|
|
||||||
{
|
|
||||||
metrics
|
|
||||||
.AddAspNetCoreInstrumentation()
|
|
||||||
.AddHttpClientInstrumentation()
|
|
||||||
.AddRuntimeInstrumentation()
|
|
||||||
.AddOtlpExporter();
|
|
||||||
});
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ using NodaTime;
|
|||||||
using NodaTime.Serialization.SystemTextJson;
|
using NodaTime.Serialization.SystemTextJson;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.RateLimiting;
|
using System.Threading.RateLimiting;
|
||||||
using DysonNetwork.Pass.Auth.OidcProvider.Options;
|
using DysonNetwork.Pass.Auth.OidcProvider.Options;
|
||||||
using DysonNetwork.Pass.Auth.OidcProvider.Services;
|
using DysonNetwork.Pass.Auth.OidcProvider.Services;
|
||||||
@@ -33,11 +34,6 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddLocalization(options => options.ResourcesPath = "Resources");
|
services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||||
|
|
||||||
services.AddDbContext<AppDatabase>();
|
services.AddDbContext<AppDatabase>();
|
||||||
services.AddSingleton<IConnectionMultiplexer>(_ =>
|
|
||||||
{
|
|
||||||
var connection = configuration.GetConnectionString("FastRetrieve")!;
|
|
||||||
return ConnectionMultiplexer.Connect(connection);
|
|
||||||
});
|
|
||||||
services.AddSingleton<IClock>(SystemClock.Instance);
|
services.AddSingleton<IClock>(SystemClock.Instance);
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
services.AddSingleton<ICacheService, CacheServiceRedis>();
|
services.AddSingleton<ICacheService, CacheServiceRedis>();
|
||||||
@@ -51,12 +47,8 @@ public static class ServiceCollectionExtensions
|
|||||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddPusherService();
|
services.AddRingService();
|
||||||
|
|
||||||
// Register gRPC services
|
|
||||||
services.AddScoped<AccountServiceGrpc>();
|
|
||||||
services.AddScoped<AuthServiceGrpc>();
|
|
||||||
|
|
||||||
// Register OIDC services
|
// Register OIDC services
|
||||||
services.AddScoped<OidcService, GoogleOidcService>();
|
services.AddScoped<OidcService, GoogleOidcService>();
|
||||||
@@ -74,6 +66,7 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
services.AddControllers().AddJsonOptions(options =>
|
services.AddControllers().AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
|
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
||||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||||
|
|
||||||
@@ -136,7 +129,8 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
Version = "v1",
|
Version = "v1",
|
||||||
Title = "Dyson Pass",
|
Title = "Dyson Pass",
|
||||||
Description = "The authentication service of the Dyson Network. Mainly handling authentication and authorization.",
|
Description =
|
||||||
|
"The authentication service of the Dyson Network. Mainly handling authentication and authorization.",
|
||||||
TermsOfService = new Uri("https://solsynth.dev/terms"),
|
TermsOfService = new Uri("https://solsynth.dev/terms"),
|
||||||
License = new OpenApiLicense
|
License = new OpenApiLicense
|
||||||
{
|
{
|
||||||
@@ -194,6 +188,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<ActionLogService>();
|
services.AddScoped<ActionLogService>();
|
||||||
services.AddScoped<AccountService>();
|
services.AddScoped<AccountService>();
|
||||||
services.AddScoped<AccountEventService>();
|
services.AddScoped<AccountEventService>();
|
||||||
|
services.AddScoped<NotableDaysService>();
|
||||||
services.AddScoped<ActionLogService>();
|
services.AddScoped<ActionLogService>();
|
||||||
services.AddScoped<RelationshipService>();
|
services.AddScoped<RelationshipService>();
|
||||||
services.AddScoped<MagicSpellService>();
|
services.AddScoped<MagicSpellService>();
|
||||||
@@ -210,6 +205,8 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
|
services.Configure<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
|
||||||
services.AddScoped<OidcProviderService>();
|
services.AddScoped<OidcProviderService>();
|
||||||
|
|
||||||
|
services.AddHostedService<BroadcastEventHandler>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ public class OrderController(PaymentService payment, AuthService auth, AppDataba
|
|||||||
var order = await db.PaymentOrders.FindAsync(id);
|
var order = await db.PaymentOrders.FindAsync(id);
|
||||||
|
|
||||||
if (order == null)
|
if (order == null)
|
||||||
{
|
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(order);
|
return Ok(order);
|
||||||
}
|
}
|
||||||
@@ -26,8 +24,7 @@ public class OrderController(PaymentService payment, AuthService auth, AppDataba
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<Order>> PayOrder(Guid id, [FromBody] PayOrderRequest request)
|
public async Task<ActionResult<Order>> PayOrder(Guid id, [FromBody] PayOrderRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser ||
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
|
|
||||||
|
|
||||||
// Validate PIN code
|
// Validate PIN code
|
||||||
if (!await auth.ValidatePinCode(currentUser.Id, request.PinCode))
|
if (!await auth.ValidatePinCode(currentUser.Id, request.PinCode))
|
||||||
@@ -41,7 +38,7 @@ public class OrderController(PaymentService payment, AuthService auth, AppDataba
|
|||||||
return BadRequest("Wallet was not found.");
|
return BadRequest("Wallet was not found.");
|
||||||
|
|
||||||
// Pay the order
|
// Pay the order
|
||||||
var paidOrder = await payment.PayOrderAsync(id, wallet.Id);
|
var paidOrder = await payment.PayOrderAsync(id, wallet);
|
||||||
return Ok(paidOrder);
|
return Ok(paidOrder);
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Globalization;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.Protobuf;
|
using NodaTime.Serialization.Protobuf;
|
||||||
@@ -23,11 +24,14 @@ public enum OrderStatus
|
|||||||
|
|
||||||
public class Order : ModelBase
|
public class Order : ModelBase
|
||||||
{
|
{
|
||||||
|
public const string InternalAppIdentifier = "internal";
|
||||||
|
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
public OrderStatus Status { get; set; } = OrderStatus.Unpaid;
|
public OrderStatus Status { get; set; } = OrderStatus.Unpaid;
|
||||||
[MaxLength(128)] public string Currency { get; set; } = null!;
|
[MaxLength(128)] public string Currency { get; set; } = null!;
|
||||||
[MaxLength(4096)] public string? Remarks { get; set; }
|
[MaxLength(4096)] public string? Remarks { get; set; }
|
||||||
[MaxLength(4096)] public string? AppIdentifier { get; set; }
|
[MaxLength(4096)] public string? AppIdentifier { get; set; }
|
||||||
|
[MaxLength(4096)] public string? ProductIdentifier { get; set; }
|
||||||
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
|
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; }
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
public Instant ExpiredAt { get; set; }
|
public Instant ExpiredAt { get; set; }
|
||||||
@@ -44,10 +48,11 @@ public class Order : ModelBase
|
|||||||
Currency = Currency,
|
Currency = Currency,
|
||||||
Remarks = Remarks,
|
Remarks = Remarks,
|
||||||
AppIdentifier = AppIdentifier,
|
AppIdentifier = AppIdentifier,
|
||||||
|
ProductIdentifier = ProductIdentifier,
|
||||||
Meta = Meta == null
|
Meta = Meta == null
|
||||||
? null
|
? null
|
||||||
: Google.Protobuf.ByteString.CopyFrom(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(Meta)),
|
: Google.Protobuf.ByteString.CopyFrom(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(Meta)),
|
||||||
Amount = Amount.ToString(),
|
Amount = Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
ExpiredAt = ExpiredAt.ToTimestamp(),
|
ExpiredAt = ExpiredAt.ToTimestamp(),
|
||||||
PayeeWalletId = PayeeWalletId?.ToString(),
|
PayeeWalletId = PayeeWalletId?.ToString(),
|
||||||
TransactionId = TransactionId?.ToString(),
|
TransactionId = TransactionId?.ToString(),
|
||||||
@@ -61,13 +66,14 @@ public class Order : ModelBase
|
|||||||
Currency = proto.Currency,
|
Currency = proto.Currency,
|
||||||
Remarks = proto.Remarks,
|
Remarks = proto.Remarks,
|
||||||
AppIdentifier = proto.AppIdentifier,
|
AppIdentifier = proto.AppIdentifier,
|
||||||
|
ProductIdentifier = proto.ProductIdentifier,
|
||||||
Meta = proto.HasMeta
|
Meta = proto.HasMeta
|
||||||
? System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(proto.Meta.ToByteArray())
|
? System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(proto.Meta.ToByteArray())
|
||||||
: null,
|
: null,
|
||||||
Amount = decimal.Parse(proto.Amount),
|
Amount = decimal.Parse(proto.Amount),
|
||||||
ExpiredAt = proto.ExpiredAt.ToInstant(),
|
ExpiredAt = proto.ExpiredAt.ToInstant(),
|
||||||
PayeeWalletId = proto.HasPayeeWalletId ? Guid.Parse(proto.PayeeWalletId) : null,
|
PayeeWalletId = proto.PayeeWalletId is not null ? Guid.Parse(proto.PayeeWalletId) : null,
|
||||||
TransactionId = proto.HasTransactionId ? Guid.Parse(proto.TransactionId) : null,
|
TransactionId = proto.TransactionId is not null ? Guid.Parse(proto.TransactionId) : null,
|
||||||
Transaction = proto.Transaction is not null ? Transaction.FromProtoValue(proto.Transaction) : null,
|
Transaction = proto.Transaction is not null ? Transaction.FromProtoValue(proto.Transaction) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -100,7 +106,7 @@ public class Transaction : ModelBase
|
|||||||
{
|
{
|
||||||
Id = Id.ToString(),
|
Id = Id.ToString(),
|
||||||
Currency = Currency,
|
Currency = Currency,
|
||||||
Amount = Amount.ToString(),
|
Amount = Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
Remarks = Remarks,
|
Remarks = Remarks,
|
||||||
Type = (Shared.Proto.TransactionType)Type,
|
Type = (Shared.Proto.TransactionType)Type,
|
||||||
PayerWalletId = PayerWalletId?.ToString(),
|
PayerWalletId = PayerWalletId?.ToString(),
|
||||||
@@ -114,7 +120,7 @@ public class Transaction : ModelBase
|
|||||||
Amount = decimal.Parse(proto.Amount),
|
Amount = decimal.Parse(proto.Amount),
|
||||||
Remarks = proto.Remarks,
|
Remarks = proto.Remarks,
|
||||||
Type = (TransactionType)proto.Type,
|
Type = (TransactionType)proto.Type,
|
||||||
PayerWalletId = proto.HasPayerWalletId ? Guid.Parse(proto.PayerWalletId) : null,
|
PayerWalletId = proto.PayerWalletId is not null ? Guid.Parse(proto.PayerWalletId) : null,
|
||||||
PayeeWalletId = proto.HasPayeeWalletId ? Guid.Parse(proto.PayeeWalletId) : null,
|
PayeeWalletId = proto.PayeeWalletId is not null ? Guid.Parse(proto.PayeeWalletId) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
using DysonNetwork.Pass.Localization;
|
using DysonNetwork.Pass.Localization;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
|
using DysonNetwork.Shared.Stream;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Storage;
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
|
using NATS.Client.Core;
|
||||||
|
using NATS.Client.JetStream;
|
||||||
|
using NATS.Net;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using AccountService = DysonNetwork.Pass.Account.AccountService;
|
using AccountService = DysonNetwork.Pass.Account.AccountService;
|
||||||
|
|
||||||
@@ -12,8 +17,9 @@ namespace DysonNetwork.Pass.Wallet;
|
|||||||
public class PaymentService(
|
public class PaymentService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
WalletService wat,
|
WalletService wat,
|
||||||
PusherService.PusherServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
|
INatsConnection nats
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public async Task<Order> CreateOrderAsync(
|
public async Task<Order> CreateOrderAsync(
|
||||||
@@ -22,6 +28,8 @@ public class PaymentService(
|
|||||||
decimal amount,
|
decimal amount,
|
||||||
Duration? expiration = null,
|
Duration? expiration = null,
|
||||||
string? appIdentifier = null,
|
string? appIdentifier = null,
|
||||||
|
string? productIdentifier = null,
|
||||||
|
string? remarks = null,
|
||||||
Dictionary<string, object>? meta = null,
|
Dictionary<string, object>? meta = null,
|
||||||
bool reuseable = true
|
bool reuseable = true
|
||||||
)
|
)
|
||||||
@@ -29,26 +37,25 @@ public class PaymentService(
|
|||||||
// Check if there's an existing unpaid order that can be reused
|
// Check if there's an existing unpaid order that can be reused
|
||||||
if (reuseable && appIdentifier != null)
|
if (reuseable && appIdentifier != null)
|
||||||
{
|
{
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
var existingOrder = await db.PaymentOrders
|
var existingOrder = await db.PaymentOrders
|
||||||
.Where(o => o.Status == OrderStatus.Unpaid &&
|
.Where(o => o.Status == OrderStatus.Unpaid &&
|
||||||
o.PayeeWalletId == payeeWalletId &&
|
o.PayeeWalletId == payeeWalletId &&
|
||||||
o.Currency == currency &&
|
o.Currency == currency &&
|
||||||
o.Amount == amount &&
|
o.Amount == amount &&
|
||||||
o.AppIdentifier == appIdentifier &&
|
o.AppIdentifier == appIdentifier &&
|
||||||
o.ExpiredAt > SystemClock.Instance.GetCurrentInstant())
|
o.ProductIdentifier == productIdentifier &&
|
||||||
|
o.ExpiredAt > now)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
// If an existing order is found, check if meta matches
|
// If an existing order is found, check if meta matches
|
||||||
if (existingOrder != null && meta != null && existingOrder.Meta != null)
|
if (existingOrder != null && meta != null && existingOrder.Meta != null)
|
||||||
{
|
{
|
||||||
// Compare meta dictionaries - if they are equivalent, reuse the order
|
// Compare the meta dictionary - if they are equivalent, reuse the order
|
||||||
var metaMatches = existingOrder.Meta.Count == meta.Count &&
|
var metaMatches = existingOrder.Meta.Count == meta.Count &&
|
||||||
!existingOrder.Meta.Except(meta).Any();
|
!existingOrder.Meta.Except(meta).Any();
|
||||||
|
|
||||||
if (metaMatches)
|
if (metaMatches)
|
||||||
{
|
|
||||||
return existingOrder;
|
return existingOrder;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +67,8 @@ public class PaymentService(
|
|||||||
Amount = amount,
|
Amount = amount,
|
||||||
ExpiredAt = SystemClock.Instance.GetCurrentInstant().Plus(expiration ?? Duration.FromHours(24)),
|
ExpiredAt = SystemClock.Instance.GetCurrentInstant().Plus(expiration ?? Duration.FromHours(24)),
|
||||||
AppIdentifier = appIdentifier,
|
AppIdentifier = appIdentifier,
|
||||||
|
ProductIdentifier = productIdentifier,
|
||||||
|
Remarks = remarks,
|
||||||
Meta = meta
|
Meta = meta
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,7 +113,8 @@ public class PaymentService(
|
|||||||
string currency,
|
string currency,
|
||||||
decimal amount,
|
decimal amount,
|
||||||
string? remarks = null,
|
string? remarks = null,
|
||||||
TransactionType type = TransactionType.System
|
TransactionType type = TransactionType.System,
|
||||||
|
bool silent = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (payerWalletId == null && payeeWalletId == null)
|
if (payerWalletId == null && payeeWalletId == null)
|
||||||
@@ -121,8 +131,12 @@ public class PaymentService(
|
|||||||
Type = type
|
Type = type
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Wallet? payerWallet = null, payeeWallet = null;
|
||||||
|
|
||||||
if (payerWalletId.HasValue)
|
if (payerWalletId.HasValue)
|
||||||
{
|
{
|
||||||
|
payerWallet = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payerWalletId.Value);
|
||||||
|
|
||||||
var (payerPocket, isNewlyCreated) =
|
var (payerPocket, isNewlyCreated) =
|
||||||
await wat.GetOrCreateWalletPocketAsync(payerWalletId.Value, currency);
|
await wat.GetOrCreateWalletPocketAsync(payerWalletId.Value, currency);
|
||||||
|
|
||||||
@@ -137,6 +151,8 @@ public class PaymentService(
|
|||||||
|
|
||||||
if (payeeWalletId.HasValue)
|
if (payeeWalletId.HasValue)
|
||||||
{
|
{
|
||||||
|
payeeWallet = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payeeWalletId.Value);
|
||||||
|
|
||||||
var (payeePocket, isNewlyCreated) =
|
var (payeePocket, isNewlyCreated) =
|
||||||
await wat.GetOrCreateWalletPocketAsync(payeeWalletId.Value, currency, amount);
|
await wat.GetOrCreateWalletPocketAsync(payeeWalletId.Value, currency, amount);
|
||||||
|
|
||||||
@@ -149,13 +165,89 @@ public class PaymentService(
|
|||||||
|
|
||||||
db.PaymentTransactions.Add(transaction);
|
db.PaymentTransactions.Add(transaction);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (!silent)
|
||||||
|
await NotifyNewTransaction(transaction, payerWallet, payeeWallet);
|
||||||
|
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Order> PayOrderAsync(Guid orderId, Guid payerWalletId)
|
private async Task NotifyNewTransaction(Transaction transaction, Wallet? payerWallet, Wallet? payeeWallet)
|
||||||
|
{
|
||||||
|
if (payerWallet is not null)
|
||||||
|
{
|
||||||
|
var account = await db.Accounts
|
||||||
|
.Where(a => a.Id == payerWallet.AccountId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (account is null) return;
|
||||||
|
|
||||||
|
AccountService.SetCultureInfo(account);
|
||||||
|
|
||||||
|
// Due to ID is uuid, it longer than 8 words for sure
|
||||||
|
var readableTransactionId = transaction.Id.ToString().Replace("-", "")[..8];
|
||||||
|
var readableTransactionRemark = transaction.Remarks ?? $"#{readableTransactionId}";
|
||||||
|
|
||||||
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
|
new SendPushNotificationToUserRequest
|
||||||
|
{
|
||||||
|
UserId = account.Id.ToString(),
|
||||||
|
Notification = new PushNotification
|
||||||
|
{
|
||||||
|
Topic = "wallets.transactions",
|
||||||
|
Title = localizer["TransactionNewTitle", readableTransactionRemark],
|
||||||
|
Body = transaction.Amount > 0
|
||||||
|
? localizer["TransactionNewBodyMinus",
|
||||||
|
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
|
transaction.Currency]
|
||||||
|
: localizer["TransactionNewBodyPlus",
|
||||||
|
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
|
transaction.Currency],
|
||||||
|
IsSavable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payeeWallet is not null)
|
||||||
|
{
|
||||||
|
var account = await db.Accounts
|
||||||
|
.Where(a => a.Id == payeeWallet.AccountId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (account is null) return;
|
||||||
|
|
||||||
|
AccountService.SetCultureInfo(account);
|
||||||
|
|
||||||
|
// Due to ID is uuid, it longer than 8 words for sure
|
||||||
|
var readableTransactionId = transaction.Id.ToString().Replace("-", "")[..8];
|
||||||
|
var readableTransactionRemark = transaction.Remarks ?? $"#{readableTransactionId}";
|
||||||
|
|
||||||
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
|
new SendPushNotificationToUserRequest
|
||||||
|
{
|
||||||
|
UserId = account.Id.ToString(),
|
||||||
|
Notification = new PushNotification
|
||||||
|
{
|
||||||
|
Topic = "wallets.transactions",
|
||||||
|
Title = localizer["TransactionNewTitle", readableTransactionRemark],
|
||||||
|
Body = transaction.Amount > 0
|
||||||
|
? localizer["TransactionNewBodyPlus",
|
||||||
|
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
|
transaction.Currency]
|
||||||
|
: localizer["TransactionNewBodyMinus",
|
||||||
|
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
|
transaction.Currency],
|
||||||
|
IsSavable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Order> PayOrderAsync(Guid orderId, Wallet payerWallet)
|
||||||
{
|
{
|
||||||
var order = await db.PaymentOrders
|
var order = await db.PaymentOrders
|
||||||
.Include(o => o.Transaction)
|
.Include(o => o.Transaction)
|
||||||
|
.Include(o => o.PayeeWallet)
|
||||||
.FirstOrDefaultAsync(o => o.Id == orderId);
|
.FirstOrDefaultAsync(o => o.Id == orderId);
|
||||||
|
|
||||||
if (order == null)
|
if (order == null)
|
||||||
@@ -163,6 +255,27 @@ public class PaymentService(
|
|||||||
throw new InvalidOperationException("Order not found");
|
throw new InvalidOperationException("Order not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var js = nats.CreateJetStreamContext();
|
||||||
|
|
||||||
|
if (order.Status == OrderStatus.Paid)
|
||||||
|
{
|
||||||
|
await js.PublishAsync(
|
||||||
|
PaymentOrderEventBase.Type,
|
||||||
|
GrpcTypeHelper.ConvertObjectToByteString(new PaymentOrderEvent
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
WalletId = payerWallet.Id,
|
||||||
|
AccountId = payerWallet.AccountId,
|
||||||
|
AppIdentifier = order.AppIdentifier,
|
||||||
|
ProductIdentifier = order.ProductIdentifier,
|
||||||
|
Meta = order.Meta ?? [],
|
||||||
|
Status = (int)order.Status,
|
||||||
|
}).ToByteArray()
|
||||||
|
);
|
||||||
|
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
if (order.Status != OrderStatus.Unpaid)
|
if (order.Status != OrderStatus.Unpaid)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Order is in invalid status: {order.Status}");
|
throw new InvalidOperationException($"Order is in invalid status: {order.Status}");
|
||||||
@@ -176,51 +289,101 @@ public class PaymentService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var transaction = await CreateTransactionAsync(
|
var transaction = await CreateTransactionAsync(
|
||||||
payerWalletId,
|
payerWallet.Id,
|
||||||
order.PayeeWalletId,
|
order.PayeeWalletId,
|
||||||
order.Currency,
|
order.Currency,
|
||||||
order.Amount,
|
order.Amount,
|
||||||
order.Remarks ?? $"Payment for Order #{order.Id}",
|
order.Remarks ?? $"Payment for Order #{order.Id}",
|
||||||
type: TransactionType.Order);
|
type: TransactionType.Order,
|
||||||
|
silent: true);
|
||||||
|
|
||||||
order.TransactionId = transaction.Id;
|
order.TransactionId = transaction.Id;
|
||||||
order.Transaction = transaction;
|
order.Transaction = transaction;
|
||||||
order.Status = OrderStatus.Paid;
|
order.Status = OrderStatus.Paid;
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await NotifyOrderPaid(order);
|
await NotifyOrderPaid(order, payerWallet, order.PayeeWallet);
|
||||||
|
|
||||||
|
await js.PublishAsync(
|
||||||
|
PaymentOrderEventBase.Type,
|
||||||
|
GrpcTypeHelper.ConvertObjectToByteString(new PaymentOrderEvent
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
WalletId = payerWallet.Id,
|
||||||
|
AccountId = payerWallet.AccountId,
|
||||||
|
AppIdentifier = order.AppIdentifier,
|
||||||
|
ProductIdentifier = order.ProductIdentifier,
|
||||||
|
Meta = order.Meta ?? [],
|
||||||
|
Status = (int)order.Status,
|
||||||
|
}).ToByteArray()
|
||||||
|
);
|
||||||
|
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task NotifyOrderPaid(Order order)
|
private async Task NotifyOrderPaid(Order order, Wallet? payerWallet, Wallet? payeeWallet)
|
||||||
{
|
{
|
||||||
if (order.PayeeWallet is null) return;
|
if (payerWallet is not null)
|
||||||
var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == order.PayeeWallet.AccountId);
|
{
|
||||||
if (account is null) return;
|
var account = await db.Accounts
|
||||||
|
.Where(a => a.Id == payerWallet.AccountId)
|
||||||
AccountService.SetCultureInfo(account);
|
.FirstOrDefaultAsync();
|
||||||
|
if (account is null) return;
|
||||||
|
|
||||||
// Due to ID is uuid, it longer than 8 words for sure
|
AccountService.SetCultureInfo(account);
|
||||||
var readableOrderId = order.Id.ToString().Replace("-", "")[..8];
|
|
||||||
var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}";
|
|
||||||
|
|
||||||
|
// Due to ID is uuid, it longer than 8 words for sure
|
||||||
await pusher.SendPushNotificationToUserAsync(
|
var readableOrderId = order.Id.ToString().Replace("-", "")[..8];
|
||||||
new SendPushNotificationToUserRequest
|
var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}";
|
||||||
{
|
|
||||||
UserId = account.Id.ToString(),
|
|
||||||
Notification = new PushNotification
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
|
new SendPushNotificationToUserRequest
|
||||||
{
|
{
|
||||||
Topic = "wallets.orders.paid",
|
UserId = account.Id.ToString(),
|
||||||
Title = localizer["OrderPaidTitle", $"#{readableOrderId}"],
|
Notification = new PushNotification
|
||||||
Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency,
|
{
|
||||||
readableOrderRemark],
|
Topic = "wallets.orders.paid",
|
||||||
IsSavable = true
|
Title = localizer["OrderPaidTitle", $"#{readableOrderId}"],
|
||||||
|
Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
|
order.Currency,
|
||||||
|
readableOrderRemark],
|
||||||
|
IsSavable = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
if (payeeWallet is not null)
|
||||||
|
{
|
||||||
|
var account = await db.Accounts
|
||||||
|
.Where(a => a.Id == payeeWallet.AccountId)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (account is null) return;
|
||||||
|
|
||||||
|
AccountService.SetCultureInfo(account);
|
||||||
|
|
||||||
|
// Due to ID is uuid, it longer than 8 words for sure
|
||||||
|
var readableOrderId = order.Id.ToString().Replace("-", "")[..8];
|
||||||
|
var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}";
|
||||||
|
|
||||||
|
await pusher.SendPushNotificationToUserAsync(
|
||||||
|
new SendPushNotificationToUserRequest
|
||||||
|
{
|
||||||
|
UserId = account.Id.ToString(),
|
||||||
|
Notification = new PushNotification
|
||||||
|
{
|
||||||
|
Topic = "wallets.orders.received",
|
||||||
|
Title = localizer["OrderReceivedTitle", $"#{readableOrderId}"],
|
||||||
|
Body = localizer["OrderReceivedBody", order.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
|
order.Currency,
|
||||||
|
readableOrderRemark],
|
||||||
|
IsSavable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Order> CancelOrderAsync(Guid orderId)
|
public async Task<Order> CancelOrderAsync(Guid orderId)
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ public class PaymentServiceGrpc(PaymentService paymentService) : Shared.Proto.Pa
|
|||||||
request.Currency,
|
request.Currency,
|
||||||
decimal.Parse(request.Amount),
|
decimal.Parse(request.Amount),
|
||||||
request.Expiration is not null ? Duration.FromSeconds(request.Expiration.Seconds) : null,
|
request.Expiration is not null ? Duration.FromSeconds(request.Expiration.Seconds) : null,
|
||||||
request.HasAppIdentifier ? request.AppIdentifier : null,
|
request.HasAppIdentifier ? request.AppIdentifier : Order.InternalAppIdentifier,
|
||||||
// Assuming meta is a JSON string
|
request.HasProductIdentifier ? request.ProductIdentifier : null,
|
||||||
|
request.HasRemarks ? request.Remarks : null,
|
||||||
request.HasMeta
|
request.HasMeta
|
||||||
? System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(request.Meta.ToStringUtf8())
|
? GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object>>(request.Meta)
|
||||||
: null,
|
: null,
|
||||||
request.Reuseable
|
request.Reuseable
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -150,29 +150,6 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SubscriptionOrderRequest
|
|
||||||
{
|
|
||||||
[Required] public Guid OrderId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("order/handle")]
|
|
||||||
[Authorize]
|
|
||||||
public async Task<ActionResult<Subscription>> HandleSubscriptionOrder([FromBody] SubscriptionOrderRequest request)
|
|
||||||
{
|
|
||||||
var order = await db.PaymentOrders.FindAsync(request.OrderId);
|
|
||||||
if (order is null) return NotFound($"Order with ID {request.OrderId} was not found.");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var subscription = await subscriptions.HandleSubscriptionOrder(order);
|
|
||||||
return subscription;
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
|
||||||
{
|
|
||||||
return BadRequest(ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RestorePurchaseRequest
|
public class RestorePurchaseRequest
|
||||||
{
|
{
|
||||||
[Required] public string OrderId { get; set; } = null!;
|
[Required] public string OrderId { get; set; } = null!;
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ public class SubscriptionRenewalJob(
|
|||||||
null,
|
null,
|
||||||
WalletCurrency.GoldenPoint,
|
WalletCurrency.GoldenPoint,
|
||||||
subscription.FinalPrice,
|
subscription.FinalPrice,
|
||||||
appIdentifier: SubscriptionService.SubscriptionOrderIdentifier,
|
appIdentifier: "internal",
|
||||||
|
productIdentifier: subscription.Identifier,
|
||||||
meta: new Dictionary<string, object>()
|
meta: new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
["subscription_id"] = subscription.Id.ToString(),
|
["subscription_id"] = subscription.Id.ToString(),
|
||||||
@@ -86,7 +87,7 @@ public class SubscriptionRenewalJob(
|
|||||||
if (wallet is null) continue;
|
if (wallet is null) continue;
|
||||||
|
|
||||||
// Process automatic payment from wallet
|
// Process automatic payment from wallet
|
||||||
await paymentService.PayOrderAsync(order.Id, wallet.Id);
|
await paymentService.PayOrderAsync(order.Id, wallet);
|
||||||
|
|
||||||
// Update subscription details
|
// Update subscription details
|
||||||
subscription.BegunAt = subscription.EndedAt!.Value;
|
subscription.BegunAt = subscription.EndedAt!.Value;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class SubscriptionService(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
PaymentService payment,
|
PaymentService payment,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
PusherService.PusherServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
@@ -229,8 +229,6 @@ public class SubscriptionService(
|
|||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string SubscriptionOrderIdentifier = "solian.subscription.order";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a subscription order for an unpaid or expired subscription.
|
/// Creates a subscription order for an unpaid or expired subscription.
|
||||||
/// If the subscription is active, it will extend its expiration date.
|
/// If the subscription is active, it will extend its expiration date.
|
||||||
@@ -259,7 +257,8 @@ public class SubscriptionService(
|
|||||||
null,
|
null,
|
||||||
subscriptionInfo.Currency,
|
subscriptionInfo.Currency,
|
||||||
subscription.FinalPrice,
|
subscription.FinalPrice,
|
||||||
appIdentifier: SubscriptionOrderIdentifier,
|
appIdentifier: "internal",
|
||||||
|
productIdentifier: identifier,
|
||||||
meta: new Dictionary<string, object>()
|
meta: new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
["subscription_id"] = subscription.Id.ToString(),
|
["subscription_id"] = subscription.Id.ToString(),
|
||||||
@@ -270,8 +269,7 @@ public class SubscriptionService(
|
|||||||
|
|
||||||
public async Task<Subscription> HandleSubscriptionOrder(Order order)
|
public async Task<Subscription> HandleSubscriptionOrder(Order order)
|
||||||
{
|
{
|
||||||
if (order.AppIdentifier != SubscriptionOrderIdentifier || order.Status != OrderStatus.Paid ||
|
if (order.Status != OrderStatus.Paid || order.Meta?["subscription_id"] is not JsonElement subscriptionIdJson)
|
||||||
order.Meta?["subscription_id"] is not JsonElement subscriptionIdJson)
|
|
||||||
throw new InvalidOperationException("Invalid order.");
|
throw new InvalidOperationException("Invalid order.");
|
||||||
|
|
||||||
var subscriptionId = Guid.TryParse(subscriptionIdJson.ToString(), out var parsedSubscriptionId)
|
var subscriptionId = Guid.TryParse(subscriptionIdJson.ToString(), out var parsedSubscriptionId)
|
||||||
|
|||||||
@@ -46,22 +46,51 @@ public class WalletController(AppDatabase db, WalletService ws, PaymentService p
|
|||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
var query = db.PaymentTransactions.AsQueryable()
|
var accountWallet = await db.Wallets.Where(w => w.AccountId == currentUser.Id).FirstOrDefaultAsync();
|
||||||
.Include(t => t.PayeeWallet)
|
if (accountWallet is null) return NotFound();
|
||||||
.Include(t => t.PayerWallet)
|
|
||||||
.Where(t => (t.PayeeWallet != null && t.PayeeWallet.AccountId == currentUser.Id) ||
|
var query = db.PaymentTransactions
|
||||||
(t.PayerWallet != null && t.PayerWallet.AccountId == currentUser.Id));
|
.Where(t => t.PayeeWalletId == accountWallet.Id || t.PayerWalletId == accountWallet.Id)
|
||||||
|
.OrderByDescending(t => t.CreatedAt)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
var transactionCount = await query.CountAsync();
|
var transactionCount = await query.CountAsync();
|
||||||
|
Response.Headers["X-Total"] = transactionCount.ToString();
|
||||||
|
|
||||||
var transactions = await query
|
var transactions = await query
|
||||||
|
.Skip(offset)
|
||||||
|
.Take(take)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(transactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("orders")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<List<Order>>> GetOrders(
|
||||||
|
[FromQuery] int offset = 0, [FromQuery] int take = 20
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
|
var accountWallet = await db.Wallets.Where(w => w.AccountId == currentUser.Id).FirstOrDefaultAsync();
|
||||||
|
if (accountWallet is null) return NotFound();
|
||||||
|
|
||||||
|
var query = db.PaymentOrders.AsQueryable()
|
||||||
|
.Include(o => o.Transaction)
|
||||||
|
.Where(o => o.Transaction != null && (o.Transaction.PayeeWalletId == accountWallet.Id || o.Transaction.PayerWalletId == accountWallet.Id))
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
var orderCount = await query.CountAsync();
|
||||||
|
Response.Headers["X-Total"] = orderCount.ToString();
|
||||||
|
|
||||||
|
var orders = await query
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(take)
|
.Take(take)
|
||||||
.OrderByDescending(t => t.CreatedAt)
|
.OrderByDescending(t => t.CreatedAt)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
Response.Headers["X-Total"] = transactionCount.ToString();
|
return Ok(orders);
|
||||||
|
|
||||||
return Ok(transactions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WalletBalanceRequest
|
public class WalletBalanceRequest
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ public class WalletServiceGrpc(WalletService walletService) : Shared.Proto.Walle
|
|||||||
public override async Task<Shared.Proto.Wallet> GetWallet(GetWalletRequest request, ServerCallContext context)
|
public override async Task<Shared.Proto.Wallet> GetWallet(GetWalletRequest request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
var wallet = await walletService.GetWalletAsync(Guid.Parse(request.AccountId));
|
var wallet = await walletService.GetWalletAsync(Guid.Parse(request.AccountId));
|
||||||
if (wallet == null)
|
return wallet == null ? throw new RpcException(new Status(StatusCode.NotFound, "Wallet not found.")) : wallet.ToProtoValue();
|
||||||
{
|
|
||||||
throw new RpcException(new Status(StatusCode.NotFound, "Wallet not found."));
|
|
||||||
}
|
|
||||||
return wallet.ToProtoValue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<Shared.Proto.Wallet> CreateWallet(CreateWalletRequest request, ServerCallContext context)
|
public override async Task<Shared.Proto.Wallet> CreateWallet(CreateWalletRequest request, ServerCallContext context)
|
||||||
|
|||||||
@@ -10,10 +10,7 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"App": "Host=localhost;Port=5432;Database=dyson_pass;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
|
"App": "Host=localhost;Port=5432;Database=dyson_pass;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
|
||||||
"FastRetrieve": "localhost:6379",
|
|
||||||
"Etcd": "etcd.orb.local:2379",
|
|
||||||
"Stream": "nats.orb.local:4222"
|
|
||||||
},
|
},
|
||||||
"Authentication": {
|
"Authentication": {
|
||||||
"Schemes": {
|
"Schemes": {
|
||||||
@@ -83,9 +80,7 @@
|
|||||||
],
|
],
|
||||||
"Service": {
|
"Service": {
|
||||||
"Name": "DysonNetwork.Pass",
|
"Name": "DysonNetwork.Pass",
|
||||||
"Url": "https://localhost:7058",
|
"Url": "https://localhost:7058"
|
||||||
"ClientCert": "../Certificates/client.crt",
|
|
||||||
"ClientKey": "../Certificates/client.key"
|
|
||||||
},
|
},
|
||||||
"Etcd": {
|
"Etcd": {
|
||||||
"Insecure": true
|
"Insecure": true
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
|
||||||
WORKDIR /app
|
|
||||||
EXPOSE 8080
|
|
||||||
EXPOSE 8081
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
libkrb5-dev
|
|
||||||
|
|
||||||
USER $APP_UID
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
WORKDIR /src
|
|
||||||
COPY ["DysonNetwork.Pusher/DysonNetwork.Pusher.csproj", "DysonNetwork.Pusher/"]
|
|
||||||
RUN dotnet restore "DysonNetwork.Pusher/DysonNetwork.Pusher.csproj"
|
|
||||||
COPY . .
|
|
||||||
WORKDIR "/src/DysonNetwork.Pusher"
|
|
||||||
RUN dotnet build "./DysonNetwork.Pusher.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
|
||||||
|
|
||||||
FROM build AS publish
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
RUN dotnet publish "./DysonNetwork.Pusher.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
|
||||||
|
|
||||||
FROM base AS final
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=publish /app/publish .
|
|
||||||
ENTRYPOINT ["dotnet", "DysonNetwork.Pusher.dll"]
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using DysonNetwork.Pusher.Services;
|
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Pusher.Startup;
|
|
||||||
|
|
||||||
public static class ApplicationConfiguration
|
|
||||||
{
|
|
||||||
public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
app.MapOpenApi();
|
|
||||||
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
|
|
||||||
app.UseRequestLocalization();
|
|
||||||
|
|
||||||
ConfigureForwardedHeaders(app, configuration);
|
|
||||||
|
|
||||||
app.UseCors(opts =>
|
|
||||||
opts.SetIsOriginAllowed(_ => true)
|
|
||||||
.WithExposedHeaders("*")
|
|
||||||
.WithHeaders("*")
|
|
||||||
.AllowCredentials()
|
|
||||||
.AllowAnyHeader()
|
|
||||||
.AllowAnyMethod()
|
|
||||||
);
|
|
||||||
|
|
||||||
app.UseWebSockets();
|
|
||||||
app.UseRateLimiter();
|
|
||||||
app.UseAuthentication();
|
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
app.MapControllers().RequireRateLimiting("fixed");
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConfigureForwardedHeaders(WebApplication app, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
var knownProxiesSection = configuration.GetSection("KnownProxies");
|
|
||||||
var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
|
|
||||||
|
|
||||||
if (knownProxiesSection.Exists())
|
|
||||||
{
|
|
||||||
var proxyAddresses = knownProxiesSection.Get<string[]>();
|
|
||||||
if (proxyAddresses != null)
|
|
||||||
foreach (var proxy in proxyAddresses)
|
|
||||||
if (IPAddress.TryParse(proxy, out var ipAddress))
|
|
||||||
forwardedHeadersOptions.KnownProxies.Add(ipAddress);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.Any);
|
|
||||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.IPv6Any);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseForwardedHeaders(forwardedHeadersOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebApplication ConfigureGrpcServices(this WebApplication app)
|
|
||||||
{
|
|
||||||
app.MapGrpcService<PusherServiceGrpc>();
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using DysonNetwork.Pusher.Notification;
|
using DysonNetwork.Ring.Notification;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Query;
|
|||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
|
||||||
namespace DysonNetwork.Pusher;
|
namespace DysonNetwork.Ring;
|
||||||
|
|
||||||
public class AppDatabase(
|
public class AppDatabase(
|
||||||
DbContextOptions<AppDatabase> options,
|
DbContextOptions<AppDatabase> options,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace DysonNetwork.Pusher.Connection;
|
namespace DysonNetwork.Ring.Connection;
|
||||||
|
|
||||||
public class ClientTypeMiddleware(RequestDelegate next)
|
public class ClientTypeMiddleware(RequestDelegate next)
|
||||||
{
|
{
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
|
|
||||||
namespace DysonNetwork.Pusher.Connection;
|
namespace DysonNetwork.Ring.Connection;
|
||||||
|
|
||||||
public interface IWebSocketPacketHandler
|
public interface IWebSocketPacketHandler
|
||||||
{
|
{
|
||||||
@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
|
||||||
namespace DysonNetwork.Pusher.Connection;
|
namespace DysonNetwork.Ring.Connection;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext> logger) : ControllerBase
|
public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext> logger) : ControllerBase
|
||||||
@@ -24,7 +24,7 @@ public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext>
|
|||||||
}
|
}
|
||||||
|
|
||||||
var accountId = currentUser.Id!;
|
var accountId = currentUser.Id!;
|
||||||
var deviceId = currentSession.Challenge.DeviceId!;
|
var deviceId = currentSession.Challenge?.DeviceId ?? Guid.NewGuid().ToString();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(deviceId))
|
if (string.IsNullOrEmpty(deviceId))
|
||||||
{
|
{
|
||||||
@@ -32,7 +32,8 @@ public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(new WebSocketAcceptContext
|
||||||
|
{ KeepAliveInterval = TimeSpan.FromSeconds(60) });
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
var connectionKey = (accountId, deviceId);
|
var connectionKey = (accountId, deviceId);
|
||||||
|
|
||||||
@@ -65,7 +66,12 @@ public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext>
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "WebSocket disconnected with user @{UserName}#{UserId} and device #{DeviceId} unexpectedly");
|
logger.LogError(ex,
|
||||||
|
"WebSocket disconnected with user @{UserName}#{UserId} and device #{DeviceId} unexpectedly",
|
||||||
|
currentUser.Name,
|
||||||
|
currentUser.Id,
|
||||||
|
deviceId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -99,7 +105,7 @@ public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext>
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
var packet = WebSocketPacket.FromBytes(buffer[..receiveResult.Count]);
|
var packet = WebSocketPacket.FromBytes(buffer[..receiveResult.Count]);
|
||||||
_ = ws.HandlePacket(currentUser, connectionKey.DeviceId, packet, webSocket);
|
await ws.HandlePacket(currentUser, connectionKey.DeviceId, packet, webSocket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
@@ -113,4 +119,4 @@ public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ using DysonNetwork.Shared.Proto;
|
|||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.SystemTextJson;
|
using NodaTime.Serialization.SystemTextJson;
|
||||||
|
|
||||||
namespace DysonNetwork.Pusher.Connection;
|
namespace DysonNetwork.Ring.Connection;
|
||||||
|
|
||||||
public class WebSocketPacket
|
public class WebSocketPacket
|
||||||
{
|
{
|
||||||
@@ -29,6 +29,7 @@ public class WebSocketPacket
|
|||||||
var json = System.Text.Encoding.UTF8.GetString(bytes);
|
var json = System.Text.Encoding.UTF8.GetString(bytes);
|
||||||
var jsonOpts = new JsonSerializerOptions
|
var jsonOpts = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
};
|
};
|
||||||
@@ -48,6 +49,7 @@ public class WebSocketPacket
|
|||||||
|
|
||||||
var jsonOpts = new JsonSerializerOptions
|
var jsonOpts = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
};
|
};
|
||||||
@@ -65,6 +67,7 @@ public class WebSocketPacket
|
|||||||
{
|
{
|
||||||
var jsonOpts = new JsonSerializerOptions
|
var jsonOpts = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||||
@@ -1,27 +1,23 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using dotnet_etcd.interfaces;
|
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
|
||||||
namespace DysonNetwork.Pusher.Connection;
|
namespace DysonNetwork.Ring.Connection;
|
||||||
|
|
||||||
public class WebSocketService
|
public class WebSocketService
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<WebSocketService> _logger;
|
private readonly ILogger<WebSocketService> _logger;
|
||||||
private readonly IEtcdClient _etcdClient;
|
|
||||||
private readonly IDictionary<string, IWebSocketPacketHandler> _handlerMap;
|
private readonly IDictionary<string, IWebSocketPacketHandler> _handlerMap;
|
||||||
|
|
||||||
public WebSocketService(
|
public WebSocketService(
|
||||||
IEnumerable<IWebSocketPacketHandler> handlers,
|
IEnumerable<IWebSocketPacketHandler> handlers,
|
||||||
IEtcdClient etcdClient,
|
|
||||||
ILogger<WebSocketService> logger,
|
ILogger<WebSocketService> logger,
|
||||||
IConfiguration configuration
|
IConfiguration configuration
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_etcdClient = etcdClient;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_handlerMap = handlers.ToDictionary(h => h.PacketType);
|
_handlerMap = handlers.ToDictionary(h => h.PacketType);
|
||||||
@@ -49,11 +45,20 @@ public class WebSocketService
|
|||||||
public void Disconnect((string AccountId, string DeviceId) key, string? reason = null)
|
public void Disconnect((string AccountId, string DeviceId) key, string? reason = null)
|
||||||
{
|
{
|
||||||
if (!ActiveConnections.TryGetValue(key, out var data)) return;
|
if (!ActiveConnections.TryGetValue(key, out var data)) return;
|
||||||
data.Socket.CloseAsync(
|
try
|
||||||
WebSocketCloseStatus.NormalClosure,
|
{
|
||||||
reason ?? "Server just decided to disconnect.",
|
data.Socket.CloseAsync(
|
||||||
CancellationToken.None
|
WebSocketCloseStatus.NormalClosure,
|
||||||
);
|
reason ?? "Server just decided to disconnect.",
|
||||||
|
CancellationToken.None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error while closing WebSocket for {AccountId}:{DeviceId}", key.AccountId,
|
||||||
|
key.DeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
data.Cts.Cancel();
|
data.Cts.Cancel();
|
||||||
ActiveConnections.TryRemove(key, out _);
|
ActiveConnections.TryRemove(key, out _);
|
||||||
}
|
}
|
||||||
@@ -133,45 +138,24 @@ public class WebSocketService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get the service URL from etcd for the specified endpoint
|
var serviceUrl = "https://" + packet.Endpoint;
|
||||||
var serviceKey = $"/services/{packet.Endpoint}";
|
|
||||||
var response = await _etcdClient.GetAsync(serviceKey);
|
|
||||||
|
|
||||||
if (response.Kvs.Count > 0)
|
var callInvoker = GrpcClientHelper.CreateCallInvoker(serviceUrl);
|
||||||
|
var client = new RingHandlerService.RingHandlerServiceClient(callInvoker);
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var serviceUrl = response.Kvs[0].Value.ToStringUtf8();
|
await client.ReceiveWebSocketPacketAsync(new ReceiveWebSocketPacketRequest
|
||||||
|
|
||||||
var clientCertPath = _configuration["Service:ClientCert"]!;
|
|
||||||
var clientKeyPath = _configuration["Service:ClientKey"]!;
|
|
||||||
var clientCertPassword = _configuration["Service:CertPassword"];
|
|
||||||
|
|
||||||
var callInvoker =
|
|
||||||
GrpcClientHelper.CreateCallInvoker(
|
|
||||||
serviceUrl,
|
|
||||||
clientCertPath,
|
|
||||||
clientKeyPath,
|
|
||||||
clientCertPassword
|
|
||||||
);
|
|
||||||
var client = new PusherHandlerService.PusherHandlerServiceClient(callInvoker);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
await client.ReceiveWebSocketPacketAsync(new ReceiveWebSocketPacketRequest
|
Account = currentUser,
|
||||||
{
|
DeviceId = deviceId,
|
||||||
Account = currentUser,
|
Packet = packet.ToProtoValue()
|
||||||
DeviceId = deviceId,
|
});
|
||||||
Packet = packet.ToProtoValue()
|
}
|
||||||
});
|
catch (RpcException ex)
|
||||||
}
|
{
|
||||||
catch (RpcException ex)
|
_logger.LogError(ex, $"Error forwarding packet to endpoint: {packet.Endpoint}");
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Error forwarding packet to endpoint: {packet.Endpoint}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogWarning($"No service registered for endpoint: {packet.Endpoint}");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -190,4 +174,4 @@ public class WebSocketService
|
|||||||
CancellationToken.None
|
CancellationToken.None
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
27
DysonNetwork.Ring/Dockerfile
Normal file
27
DysonNetwork.Ring/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8080
|
||||||
|
EXPOSE 8081
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libkrb5-dev
|
||||||
|
|
||||||
|
USER $APP_UID
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["DysonNetwork.Ring/DysonNetwork.Ring.csproj", "DysonNetwork.Ring/"]
|
||||||
|
RUN dotnet restore "DysonNetwork.Ring/DysonNetwork.Ring.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/DysonNetwork.Ring"
|
||||||
|
RUN dotnet build "./DysonNetwork.Ring.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
RUN dotnet publish "./DysonNetwork.Ring.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "DysonNetwork.Ring.dll"]
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<RootNamespace>DysonNetwork.Pusher</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj" />
|
||||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user