Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05099cce91 | |||
| 97f6522497 | |||
| e3bbcbe4a4 | |||
| 12b1a7f1e2 | |||
| 157cc457a9 | |||
| fa7486915b | |||
| 1794d4e777 | |||
| 8426091e91 | |||
| fa7dac9ec1 | |||
| 67970e5d23 | |||
| fc5b116263 | |||
| e2ec948c74 | |||
| c6a4357c6f | |||
| 6704505dae | |||
| 269a600869 | |||
| b3a06ce5e8 | |||
| 1b0a9a62b4 | |||
| babfd2833f | |||
| 78ff4481db | |||
| dccefb0158 | |||
| 49cf91f77d | |||
| 46e2d879cd | |||
| 26d183af7d | |||
| 35459fdfdd | |||
| 94bad2912b | |||
| 44f53e80b8 | |||
| 44282f8f3b | |||
| bd4ceada65 | |||
| 86be2d004f | |||
| 3c4c2e660a | |||
| 08c091c203 | |||
| 5f523db485 |
@@ -0,0 +1,78 @@
|
|||||||
|
# Disabled template. GitHub ignores files outside .github/workflows/.
|
||||||
|
# Move this file to .github/workflows/endpoint-contract.yml when it should run.
|
||||||
|
name: Endpoint Contract
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
endpoint-contract:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
ASPNETCORE_ENVIRONMENT: Development
|
||||||
|
ASPNETCORE_URLS: http://127.0.0.1:18080
|
||||||
|
Database__Provider: sqlite
|
||||||
|
Database__SqlitePath: ${{ runner.temp }}/dorknet-endpoint-contract.db
|
||||||
|
DORKNET_JWT_SECRET: endpoint-contract-secret-000000000000000000000000000000000000000000
|
||||||
|
S3__Endpoint: ""
|
||||||
|
S3__AccessKey: ""
|
||||||
|
S3__SecretKey: ""
|
||||||
|
PROTOBUF_PROTOC: /usr/bin/protoc
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
|
- name: Install protobuf compiler
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y protobuf-compiler
|
||||||
|
|
||||||
|
- name: Build server
|
||||||
|
run: dotnet build DorkNet.Server/DorkNet.Server.csproj
|
||||||
|
|
||||||
|
- name: Build endpoint contract runner
|
||||||
|
run: dotnet build tools/DorkNet.EndpointContract/DorkNet.EndpointContract.csproj --no-restore
|
||||||
|
|
||||||
|
- name: Start server
|
||||||
|
run: |
|
||||||
|
dotnet DorkNet.Server/bin/Debug/net9.0/DorkNet.Server.dll > "$RUNNER_TEMP/dorknet-server.log" 2>&1 &
|
||||||
|
echo "$!" > "$RUNNER_TEMP/dorknet-server.pid"
|
||||||
|
|
||||||
|
- name: Wait for server readiness
|
||||||
|
run: |
|
||||||
|
for attempt in {1..60}; do
|
||||||
|
if curl --fail --silent http://127.0.0.1:18080/healthz; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
cat "$RUNNER_TEMP/dorknet-server.log"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Run endpoint contract
|
||||||
|
run: |
|
||||||
|
dotnet run --project tools/DorkNet.EndpointContract/DorkNet.EndpointContract.csproj --no-build -- \
|
||||||
|
--server-assembly DorkNet.Server/bin/Debug/net9.0/DorkNet.Server.dll \
|
||||||
|
--base-url http://127.0.0.1:18080 \
|
||||||
|
--report "$RUNNER_TEMP/endpoint-contract-report.json"
|
||||||
|
|
||||||
|
- name: Upload endpoint contract artifacts
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: endpoint-contract
|
||||||
|
path: |
|
||||||
|
${{ runner.temp }}/endpoint-contract-report.json
|
||||||
|
${{ runner.temp }}/dorknet-server.log
|
||||||
|
|
||||||
|
- name: Stop server
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ -f "$RUNNER_TEMP/dorknet-server.pid" ]; then
|
||||||
|
kill "$(cat "$RUNNER_TEMP/dorknet-server.pid")" || true
|
||||||
|
fi
|
||||||
@@ -11,6 +11,9 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- 'v1-december-*'
|
- 'v1-december-*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # create the GitHub release + upload assets
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ tmp/
|
|||||||
## OS
|
## OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
.backups
|
||||||
## Reverse-engineered artifacts (Il2CppDumper / Cpp2IL output, not for commit)
|
## Reverse-engineered artifacts (Il2CppDumper / Cpp2IL output, not for commit)
|
||||||
dump.cs
|
dump.cs
|
||||||
il2cpp.h
|
il2cpp.h
|
||||||
@@ -48,6 +48,7 @@ dist$ZIPNAME.zip
|
|||||||
## time someone rebuilds. Keep the source committed, regenerate output.
|
## time someone rebuilds. Keep the source committed, regenerate output.
|
||||||
DorkNet.Server/wwwroot/admin/index.html
|
DorkNet.Server/wwwroot/admin/index.html
|
||||||
DorkNet.Server/wwwroot/admin/assets/
|
DorkNet.Server/wwwroot/admin/assets/
|
||||||
|
DorkNet.Server/wwwroot/admin/brand/
|
||||||
DorkNet.Server/wwwroot/admin/*.map
|
DorkNet.Server/wwwroot/admin/*.map
|
||||||
DorkNet.Server/admin-ui/node_modules/
|
DorkNet.Server/admin-ui/node_modules/
|
||||||
DorkNet.Server/admin-ui/dist/
|
DorkNet.Server/admin-ui/dist/
|
||||||
@@ -58,6 +59,7 @@ DorkNet.Server/admin-ui/tsconfig.tsbuildinfo
|
|||||||
## Same reasoning as admin-ui above: hashed output, regenerate per build.
|
## Same reasoning as admin-ui above: hashed output, regenerate per build.
|
||||||
DorkNet.Server/wwwroot/site/index.html
|
DorkNet.Server/wwwroot/site/index.html
|
||||||
DorkNet.Server/wwwroot/site/assets/
|
DorkNet.Server/wwwroot/site/assets/
|
||||||
|
DorkNet.Server/wwwroot/site/brand/
|
||||||
DorkNet.Server/wwwroot/site/*.map
|
DorkNet.Server/wwwroot/site/*.map
|
||||||
DorkNet.Server/site/node_modules/
|
DorkNet.Server/site/node_modules/
|
||||||
DorkNet.Server/site/dist/
|
DorkNet.Server/site/dist/
|
||||||
|
|||||||
@@ -53,12 +53,14 @@ WORKDIR /src
|
|||||||
# DorkNet.Models project ref, so its csproj needs to be present too.
|
# DorkNet.Models project ref, so its csproj needs to be present too.
|
||||||
COPY DorkNet.Server/DorkNet.Server.csproj DorkNet.Server/
|
COPY DorkNet.Server/DorkNet.Server.csproj DorkNet.Server/
|
||||||
COPY DorkNet.Models/DorkNet.Models.csproj DorkNet.Models/
|
COPY DorkNet.Models/DorkNet.Models.csproj DorkNet.Models/
|
||||||
|
COPY DorkNet.Contracts/DorkNet.Contracts.csproj DorkNet.Contracts/
|
||||||
RUN dotnet restore DorkNet.Server/DorkNet.Server.csproj
|
RUN dotnet restore DorkNet.Server/DorkNet.Server.csproj
|
||||||
|
|
||||||
# Now copy the rest of the .NET source, then drop the prebuilt admin
|
# Now copy the rest of the .NET source, then drop the prebuilt admin
|
||||||
# SPA into wwwroot/admin so the publish step ships it as static content.
|
# SPA into wwwroot/admin so the publish step ships it as static content.
|
||||||
COPY DorkNet.Server/ DorkNet.Server/
|
COPY DorkNet.Server/ DorkNet.Server/
|
||||||
COPY DorkNet.Models/ DorkNet.Models/
|
COPY DorkNet.Models/ DorkNet.Models/
|
||||||
|
COPY DorkNet.Contracts/ DorkNet.Contracts/
|
||||||
COPY --from=admin-ui /wwwroot/admin/ DorkNet.Server/wwwroot/admin/
|
COPY --from=admin-ui /wwwroot/admin/ DorkNet.Server/wwwroot/admin/
|
||||||
COPY --from=site /wwwroot/site/ DorkNet.Server/wwwroot/site/
|
COPY --from=site /wwwroot/site/ DorkNet.Server/wwwroot/site/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
ARG DOTNET_VERSION=9.0
|
||||||
|
ARG NODE_VERSION=20
|
||||||
|
|
||||||
|
FROM node:${NODE_VERSION}-alpine AS admin-ui
|
||||||
|
WORKDIR /ui
|
||||||
|
COPY DorkNet.Server/admin-ui/package.json DorkNet.Server/admin-ui/package-lock.json* ./
|
||||||
|
RUN npm install --no-fund --no-audit --loglevel=error
|
||||||
|
COPY DorkNet.Server/admin-ui/ ./
|
||||||
|
RUN mkdir -p /wwwroot/admin \
|
||||||
|
&& npm run build
|
||||||
|
|
||||||
|
FROM node:${NODE_VERSION}-alpine AS site
|
||||||
|
WORKDIR /ui
|
||||||
|
COPY DorkNet.Server/site/package.json DorkNet.Server/site/package-lock.json* ./
|
||||||
|
RUN npm install --no-fund --no-audit --loglevel=error
|
||||||
|
COPY DorkNet.Server/site/ ./
|
||||||
|
RUN mkdir -p /wwwroot/site \
|
||||||
|
&& npm run build
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS build
|
||||||
|
ARG PROJECT_PATH
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
COPY --from=admin-ui /wwwroot/admin/ DorkNet.Server/wwwroot/admin/
|
||||||
|
COPY --from=site /wwwroot/site/ DorkNet.Server/wwwroot/site/
|
||||||
|
RUN dotnet publish "${PROJECT_PATH}" -c Release \
|
||||||
|
-p:SkipAdminUIBuild=true \
|
||||||
|
-p:DorkNetServiceContainerPublish=true \
|
||||||
|
-o /app/publish
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION} AS runtime
|
||||||
|
ARG APP_DLL
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
COPY --from=build --chown=app:app /app/publish .
|
||||||
|
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production \
|
||||||
|
ASPNETCORE_URLS=http://0.0.0.0:8080 \
|
||||||
|
DOTNET_RUNNING_IN_CONTAINER=true \
|
||||||
|
DOTNET_PRINT_TELEMETRY_MESSAGE=false \
|
||||||
|
APP_DLL=${APP_DLL}
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
|
||||||
|
CMD curl --fail --silent http://localhost:8080/healthz || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["sh", "-c", "dotnet \"$APP_DLL\""]
|
||||||
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
Native Android/iOS admin app for DorkNet, built with .NET MAUI.
|
Native Android/iOS admin app for DorkNet, built with .NET MAUI.
|
||||||
|
|
||||||
|
In the microservices deployment, point the app at the public admin host
|
||||||
|
for the stack, usually `https://admin.<domain>`. Cloudflare and
|
||||||
|
`DorkNet.Gateway` keep the public URL stable. Requests made through the
|
||||||
|
admin host are handled by the `web` service's same-origin admin route;
|
||||||
|
the `/api/admin/*` path family is also exposed by the moderation slice
|
||||||
|
for non-admin-host routing.
|
||||||
|
|
||||||
|
The browser admin page, route map, and troubleshooting flow are
|
||||||
|
documented in [`../docs/admin.md`](../docs/admin.md).
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
The app stores sensitive values in platform secure storage:
|
The app stores sensitive values in platform secure storage:
|
||||||
|
|||||||
@@ -610,17 +610,7 @@ public class Mod : MelonMod
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Compute the UserData path ourselves rather than calling
|
var userData = ResolveUserDataDirectory();
|
||||||
// MelonLoader.MelonUtils.UserDataDirectory (which is renamed
|
|
||||||
// to MelonEnvironment.UserDataDirectory in newer 0.6.x builds
|
|
||||||
// and shifts again in 0.7.x). Anchoring off the mod DLL's
|
|
||||||
// location keeps this stable across versions: the DLL lives
|
|
||||||
// in <game>/Mods/, so two dirname()'s reach the game root,
|
|
||||||
// then MelonLoader/UserData/ is right next to it.
|
|
||||||
var modDll = Assembly.GetExecutingAssembly().Location;
|
|
||||||
var modsDir = Path.GetDirectoryName(modDll) ?? string.Empty;
|
|
||||||
var gameDir = Path.GetDirectoryName(modsDir) ?? string.Empty;
|
|
||||||
var userData = Path.Combine(gameDir, "MelonLoader", "UserData");
|
|
||||||
Directory.CreateDirectory(userData);
|
Directory.CreateDirectory(userData);
|
||||||
DiagnosticsLogPath = Path.Combine(userData, "dorknet-diagnostics.log");
|
DiagnosticsLogPath = Path.Combine(userData, "dorknet-diagnostics.log");
|
||||||
var path = Path.Combine(userData, "dorknet-clientmod.json");
|
var path = Path.Combine(userData, "dorknet-clientmod.json");
|
||||||
@@ -631,21 +621,21 @@ public class Mod : MelonMod
|
|||||||
}
|
}
|
||||||
var doc = JsonDocument.Parse(File.ReadAllText(path));
|
var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
var r = doc.RootElement;
|
var r = doc.RootElement;
|
||||||
if (r.TryGetProperty("ServerHost", out var v)) Cfg.ServerHost = v.GetString() ?? Cfg.ServerHost;
|
if (TryGetConfigValue(r, "ServerHost", out var v)) Cfg.ServerHost = v.GetString() ?? Cfg.ServerHost;
|
||||||
if (r.TryGetProperty("PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
|
if (TryGetConfigValue(r, "PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
|
||||||
if (r.TryGetProperty("PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
|
if (TryGetConfigValue(r, "PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
|
||||||
if (r.TryGetProperty("PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
|
if (TryGetConfigValue(r, "PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
|
||||||
if (r.TryGetProperty("EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
|
if (TryGetConfigValue(r, "EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
|
||||||
if (r.TryGetProperty("TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
|
if (TryGetConfigValue(r, "TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
|
||||||
if (r.TryGetProperty("EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
if (TryGetConfigValue(r, "EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
||||||
if (r.TryGetProperty("DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
|
if (TryGetConfigValue(r, "DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
|
||||||
if (r.TryGetProperty("DesktopScreenShareFps", out v)) Cfg.DesktopScreenShareFps = (float)v.GetDouble();
|
if (TryGetConfigValue(r, "DesktopScreenShareFps", out v)) Cfg.DesktopScreenShareFps = (float)v.GetDouble();
|
||||||
if (r.TryGetProperty("DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean();
|
if (TryGetConfigValue(r, "DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean();
|
||||||
if (r.TryGetProperty("DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
|
if (TryGetConfigValue(r, "DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
|
||||||
if (r.TryGetProperty("DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
|
if (TryGetConfigValue(r, "DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
|
||||||
if (r.TryGetProperty("DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
if (TryGetConfigValue(r, "DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
||||||
if (r.TryGetProperty("QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
|
if (TryGetConfigValue(r, "QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
|
||||||
if (r.TryGetProperty("QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array)
|
if (TryGetConfigValue(r, "QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
var rooms = new List<string>();
|
var rooms = new List<string>();
|
||||||
foreach (var e in v.EnumerateArray())
|
foreach (var e in v.EnumerateArray())
|
||||||
@@ -653,8 +643,8 @@ public class Mod : MelonMod
|
|||||||
rooms.Add(e.GetString()!);
|
rooms.Add(e.GetString()!);
|
||||||
Cfg.QuestMaxTeamSizeRooms = rooms.ToArray(); // explicit (incl. empty = none)
|
Cfg.QuestMaxTeamSizeRooms = rooms.ToArray(); // explicit (incl. empty = none)
|
||||||
}
|
}
|
||||||
if (r.TryGetProperty("EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
if (TryGetConfigValue(r, "EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
||||||
if (r.TryGetProperty("DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
if (TryGetConfigValue(r, "DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
var list = new List<string>();
|
var list = new List<string>();
|
||||||
foreach (var e in v.EnumerateArray())
|
foreach (var e in v.EnumerateArray())
|
||||||
@@ -666,7 +656,7 @@ public class Mod : MelonMod
|
|||||||
}
|
}
|
||||||
// "InjectAuthValues" key in the template is now ignored —
|
// "InjectAuthValues" key in the template is now ignored —
|
||||||
// see attic/AuthValuesInjector.cs.attic.
|
// see attic/AuthValuesInjector.cs.attic.
|
||||||
Log.Msg($"[config] loaded: ServerHost={Cfg.ServerHost}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "<unset>" : "<set>")}, " +
|
Log.Msg($"[config] loaded from {path}: ServerHost={Cfg.ServerHost}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "<unset>" : "<set>")}, " +
|
||||||
$"EnableTlsTrustBypass={Cfg.EnableTlsTrustBypass}");
|
$"EnableTlsTrustBypass={Cfg.EnableTlsTrustBypass}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -675,6 +665,44 @@ public class Mod : MelonMod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ResolveUserDataDirectory()
|
||||||
|
{
|
||||||
|
foreach (var typeName in new[]
|
||||||
|
{
|
||||||
|
"MelonLoader.MelonEnvironment, MelonLoader",
|
||||||
|
"MelonLoader.MelonUtils, MelonLoader",
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var type = Type.GetType(typeName);
|
||||||
|
var prop = type?.GetProperty("UserDataDirectory", BindingFlags.Public | BindingFlags.Static);
|
||||||
|
if (prop?.GetValue(null) is string value && !string.IsNullOrWhiteSpace(value))
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var modDll = Assembly.GetExecutingAssembly().Location;
|
||||||
|
var modsDir = Path.GetDirectoryName(modDll) ?? string.Empty;
|
||||||
|
var gameDir = Path.GetDirectoryName(modsDir) ?? AppContext.BaseDirectory;
|
||||||
|
return Path.Combine(gameDir, "MelonLoader", "UserData");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetConfigValue(JsonElement root, string name, out JsonElement value)
|
||||||
|
{
|
||||||
|
if (root.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
foreach (var prop in root.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
value = prop.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Patch dispatch helpers ────────────────────────────────────────
|
// ── Patch dispatch helpers ────────────────────────────────────────
|
||||||
private bool TryPatch(string label, Type target, string methodName,
|
private bool TryPatch(string label, Type target, string methodName,
|
||||||
Type[]? args, string? prefix = null, string? postfix = null,
|
Type[]? args, string? prefix = null, string? postfix = null,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
namespace DorkNet.Contracts;
|
||||||
|
|
||||||
|
public static class DorkNetRouteOwnership
|
||||||
|
{
|
||||||
|
private const string AdminHost = "admin";
|
||||||
|
|
||||||
|
private static readonly DorkNetRouteGroup[] ServiceRouteGroups =
|
||||||
|
[
|
||||||
|
new(
|
||||||
|
ServiceNames.Identity,
|
||||||
|
["auth", "accounts"],
|
||||||
|
[
|
||||||
|
"/.well-known",
|
||||||
|
"/account",
|
||||||
|
"/api/account",
|
||||||
|
"/api/auth",
|
||||||
|
"/api/createaccount",
|
||||||
|
"/api/platformlogin",
|
||||||
|
"/api/playerreputation",
|
||||||
|
"/api/players",
|
||||||
|
"/api/relationships",
|
||||||
|
"/api/settings",
|
||||||
|
"/cachedlogin",
|
||||||
|
"/connect",
|
||||||
|
"/eac",
|
||||||
|
"/parentalcontrol",
|
||||||
|
"/photon",
|
||||||
|
"/profileimage",
|
||||||
|
"/settings",
|
||||||
|
]),
|
||||||
|
new(
|
||||||
|
ServiceNames.Rooms,
|
||||||
|
["rooms", "match", "discovery", "lists", "roomcomments", "roomieintegrations", "studio"],
|
||||||
|
[
|
||||||
|
"/api/activities",
|
||||||
|
"/api/curatedroomplaylists",
|
||||||
|
"/api/gameconfigs",
|
||||||
|
"/api/gameconfigurations",
|
||||||
|
"/api/gamesessions",
|
||||||
|
"/api/leaderboard",
|
||||||
|
"/api/playerelo",
|
||||||
|
"/api/playlists",
|
||||||
|
"/api/roomkeys",
|
||||||
|
"/api/rooms",
|
||||||
|
"/api/royale",
|
||||||
|
"/discover",
|
||||||
|
"/featuredrooms",
|
||||||
|
"/goto",
|
||||||
|
"/hot_rooms",
|
||||||
|
"/hot_roomsandplaylists",
|
||||||
|
"/invite",
|
||||||
|
"/playlists",
|
||||||
|
"/player",
|
||||||
|
"/room",
|
||||||
|
"/roominstance",
|
||||||
|
"/rooms",
|
||||||
|
"/roomsandplaylists",
|
||||||
|
"/roomserver",
|
||||||
|
"/search_rooms",
|
||||||
|
"/search_roomsandplaylists",
|
||||||
|
"/v1/find",
|
||||||
|
"/v1/join",
|
||||||
|
"/v1/leave",
|
||||||
|
"/v1/session",
|
||||||
|
]),
|
||||||
|
new(
|
||||||
|
ServiceNames.Notify,
|
||||||
|
["notify", "chat", "platformnotifications"],
|
||||||
|
[
|
||||||
|
"/api/messages",
|
||||||
|
"/api/notification",
|
||||||
|
"/api/offlineinvite",
|
||||||
|
"/hub",
|
||||||
|
"/notify",
|
||||||
|
"/thread",
|
||||||
|
]),
|
||||||
|
new(
|
||||||
|
ServiceNames.Content,
|
||||||
|
["cdn", "data", "img", "storage", "strings-cdn", "videos"],
|
||||||
|
[
|
||||||
|
"/api/images",
|
||||||
|
"/api/photos",
|
||||||
|
"/config",
|
||||||
|
"/data",
|
||||||
|
"/img",
|
||||||
|
"/invention",
|
||||||
|
"/upload",
|
||||||
|
"/video",
|
||||||
|
]),
|
||||||
|
new(
|
||||||
|
ServiceNames.Social,
|
||||||
|
["clubs"],
|
||||||
|
[
|
||||||
|
"/announcements",
|
||||||
|
"/api/announcement",
|
||||||
|
"/api/clubreporting",
|
||||||
|
"/api/communityboard",
|
||||||
|
"/api/groups",
|
||||||
|
"/api/playercheer",
|
||||||
|
"/api/playerevents",
|
||||||
|
"/api/playersubscriptions",
|
||||||
|
"/club",
|
||||||
|
"/members",
|
||||||
|
"/subscription",
|
||||||
|
]),
|
||||||
|
new(
|
||||||
|
ServiceNames.Commerce,
|
||||||
|
["commerce", "econ"],
|
||||||
|
[
|
||||||
|
"/api/avatar",
|
||||||
|
"/api/catalog",
|
||||||
|
"/api/compatibility",
|
||||||
|
"/api/consumables",
|
||||||
|
"/api/equipment",
|
||||||
|
"/api/gamerewards",
|
||||||
|
"/api/inventions",
|
||||||
|
"/api/purchase",
|
||||||
|
"/api/storefronts",
|
||||||
|
"/commerce",
|
||||||
|
"/econ",
|
||||||
|
]),
|
||||||
|
new(
|
||||||
|
ServiceNames.Platform,
|
||||||
|
["cards", "cms", "datacollection", "gamelogs", "geo", "leaderboard", "link", "ns", "playersettings", "strings", "thorn"],
|
||||||
|
[
|
||||||
|
"/api/cards",
|
||||||
|
"/api/config",
|
||||||
|
"/api/events",
|
||||||
|
"/api/quickplay",
|
||||||
|
"/api/role",
|
||||||
|
"/api/version",
|
||||||
|
"/api/versioncheck",
|
||||||
|
"/motd",
|
||||||
|
"/pageview",
|
||||||
|
"/ping",
|
||||||
|
"/role",
|
||||||
|
"/strings",
|
||||||
|
"/v1/ping",
|
||||||
|
"/v1/player",
|
||||||
|
"/v1/regions",
|
||||||
|
"/v1/services",
|
||||||
|
]),
|
||||||
|
new(
|
||||||
|
ServiceNames.Moderation,
|
||||||
|
["bugreporting", "moderation"],
|
||||||
|
[
|
||||||
|
"/api/admin",
|
||||||
|
"/api/banappeal",
|
||||||
|
"/api/bugreporting",
|
||||||
|
"/api/moderation",
|
||||||
|
"/api/playerreporting",
|
||||||
|
"/api/playersbanned",
|
||||||
|
"/api/sanitize",
|
||||||
|
"/api/testcasemanagement",
|
||||||
|
]),
|
||||||
|
new(
|
||||||
|
ServiceNames.Web,
|
||||||
|
["www", AdminHost, "feed"],
|
||||||
|
[
|
||||||
|
"/api/site",
|
||||||
|
],
|
||||||
|
IncludesApexHost: true),
|
||||||
|
];
|
||||||
|
|
||||||
|
public static IReadOnlyList<DorkNetRouteGroup> RouteGroups => ServiceRouteGroups;
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> PublicSubdomains =>
|
||||||
|
ServiceRouteGroups
|
||||||
|
.SelectMany(group => group.HostSubdomains)
|
||||||
|
// api.* is a shared public host: it owns no service by host and is
|
||||||
|
// dispatched by path (see ResolvePublicService's path-prefix pass),
|
||||||
|
// so it never appears in a group's HostSubdomains. It must still be
|
||||||
|
// whitelisted here or HostFiltering rejects every api.{apex} request.
|
||||||
|
.Append("api")
|
||||||
|
.Append(AdminHost)
|
||||||
|
.Append("feed")
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Order(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
public static bool IsInfrastructurePath(string path)
|
||||||
|
{
|
||||||
|
return path.Equals("/healthz", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| path.StartsWith("/internal/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolvePublicService(string? host, string path, string apexDomain)
|
||||||
|
{
|
||||||
|
var normalizedPath = NormalizePath(path);
|
||||||
|
var subdomain = Subdomain(host, apexDomain);
|
||||||
|
|
||||||
|
if (string.Equals(subdomain, AdminHost, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return ServiceNames.Web;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in ServiceRouteGroups.Where(group => group.ServiceName != ServiceNames.Web))
|
||||||
|
{
|
||||||
|
if (MatchesHost(subdomain, group.HostSubdomains))
|
||||||
|
{
|
||||||
|
return group.ServiceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in ServiceRouteGroups)
|
||||||
|
{
|
||||||
|
if (StartsWithAny(normalizedPath, group.PathPrefixes))
|
||||||
|
{
|
||||||
|
return group.ServiceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var web = ServiceRouteGroups.First(group => group.ServiceName == ServiceNames.Web);
|
||||||
|
if ((subdomain is null && web.IncludesApexHost) || MatchesHost(subdomain, web.HostSubdomains))
|
||||||
|
{
|
||||||
|
return ServiceNames.Web;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServiceNames.Monolith;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsOwnedBy(string serviceName, string? host, string path, string apexDomain)
|
||||||
|
{
|
||||||
|
if (IsInfrastructurePath(path))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Equals(
|
||||||
|
ResolvePublicService(host, path, apexDomain),
|
||||||
|
serviceName,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? Subdomain(string? host, string apexDomain)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(host))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedHost = host.Split(':', 2)[0].Trim().ToLowerInvariant();
|
||||||
|
var normalizedApex = apexDomain.Split(':', 2)[0].Trim().ToLowerInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedHost)
|
||||||
|
|| string.IsNullOrWhiteSpace(normalizedApex)
|
||||||
|
|| string.Equals(normalizedHost, normalizedApex, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var suffix = "." + normalizedApex;
|
||||||
|
if (normalizedHost.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return normalizedHost[..^suffix.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
var dot = normalizedHost.IndexOf('.');
|
||||||
|
return dot > 0 ? normalizedHost[..dot] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool StartsWithAny(string path, IEnumerable<string> prefixes)
|
||||||
|
{
|
||||||
|
foreach (var prefix in prefixes)
|
||||||
|
{
|
||||||
|
if (StartsWithPathSegment(path, prefix))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool StartsWithPathSegment(string path, string prefix)
|
||||||
|
{
|
||||||
|
var normalizedPath = NormalizePath(path);
|
||||||
|
var normalizedPrefix = NormalizePath(prefix).TrimEnd('/');
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedPrefix))
|
||||||
|
{
|
||||||
|
normalizedPrefix = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedPath.Equals(normalizedPrefix, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| normalizedPath.StartsWith(normalizedPrefix + "/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesHost(string? subdomain, IEnumerable<string> candidates)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(subdomain))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates.Contains(subdomain, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizePath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return path[0] == '/' ? path : "/" + path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record DorkNetRouteGroup(
|
||||||
|
string ServiceName,
|
||||||
|
string[] HostSubdomains,
|
||||||
|
string[] PathPrefixes,
|
||||||
|
bool IncludesApexHost = false);
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
namespace DorkNet.Contracts;
|
||||||
|
|
||||||
|
public sealed class DorkNetServiceMapOptions
|
||||||
|
{
|
||||||
|
public string Identity { get; set; } = "http://localhost:8081";
|
||||||
|
public string Rooms { get; set; } = "http://localhost:8082";
|
||||||
|
public string Notify { get; set; } = "http://localhost:8083";
|
||||||
|
public string Content { get; set; } = "http://localhost:8084";
|
||||||
|
public string Social { get; set; } = "http://localhost:8085";
|
||||||
|
public string Commerce { get; set; } = "http://localhost:8086";
|
||||||
|
public string Platform { get; set; } = "http://localhost:8087";
|
||||||
|
public string Moderation { get; set; } = "http://localhost:8088";
|
||||||
|
public string Web { get; set; } = "http://localhost:8089";
|
||||||
|
public string Monolith { get; set; } = "http://localhost:8090";
|
||||||
|
|
||||||
|
public IReadOnlyList<DorkNetServiceEndpoint> Endpoints()
|
||||||
|
{
|
||||||
|
DorkNetServiceEndpoint[] endpoints =
|
||||||
|
[
|
||||||
|
new(ServiceNames.Identity, Identity),
|
||||||
|
new(ServiceNames.Rooms, Rooms),
|
||||||
|
new(ServiceNames.Notify, Notify),
|
||||||
|
new(ServiceNames.Content, Content),
|
||||||
|
new(ServiceNames.Social, Social),
|
||||||
|
new(ServiceNames.Commerce, Commerce),
|
||||||
|
new(ServiceNames.Platform, Platform),
|
||||||
|
new(ServiceNames.Moderation, Moderation),
|
||||||
|
new(ServiceNames.Web, Web),
|
||||||
|
new(ServiceNames.Monolith, Monolith),
|
||||||
|
];
|
||||||
|
|
||||||
|
return endpoints
|
||||||
|
.Where(endpoint => !string.IsNullOrWhiteSpace(endpoint.BaseUrl))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record DorkNetServiceEndpoint(string Name, string BaseUrl);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace DorkNet.Contracts;
|
||||||
|
|
||||||
|
public sealed record ServiceHealthResponse(
|
||||||
|
string Service,
|
||||||
|
string Status,
|
||||||
|
DateTimeOffset CheckedAtUtc);
|
||||||
|
|
||||||
|
public sealed record ServiceCapabilityResponse(
|
||||||
|
string Service,
|
||||||
|
string[] Owns,
|
||||||
|
string[] PlannedPublicRoutes);
|
||||||
|
|
||||||
|
public sealed record ServiceProbeResponse(
|
||||||
|
string Service,
|
||||||
|
string BaseUrl,
|
||||||
|
string Status,
|
||||||
|
int? StatusCode,
|
||||||
|
string? Error,
|
||||||
|
DateTimeOffset CheckedAtUtc);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace DorkNet.Contracts;
|
||||||
|
|
||||||
|
public static class ServiceNames
|
||||||
|
{
|
||||||
|
public const string Gateway = "gateway";
|
||||||
|
public const string Identity = "identity";
|
||||||
|
public const string Rooms = "rooms";
|
||||||
|
public const string Notify = "notify";
|
||||||
|
public const string Content = "content";
|
||||||
|
public const string Social = "social";
|
||||||
|
public const string Commerce = "commerce";
|
||||||
|
public const string Platform = "platform";
|
||||||
|
public const string Moderation = "moderation";
|
||||||
|
public const string Web = "web";
|
||||||
|
public const string Monolith = "monolith";
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Yarp.ReverseProxy.Configuration;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Gateway);
|
||||||
|
builder.Services.Configure<DorkNetServiceMapOptions>(
|
||||||
|
builder.Configuration.GetSection("DorkNet:Services"));
|
||||||
|
var serviceMap = builder.Configuration
|
||||||
|
.GetSection("DorkNet:Services")
|
||||||
|
.Get<DorkNetServiceMapOptions>() ?? new DorkNetServiceMapOptions();
|
||||||
|
var apexDomain = builder.Configuration["Domain:Apex"]
|
||||||
|
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
|
||||||
|
?? "localhost";
|
||||||
|
builder.Services
|
||||||
|
.AddReverseProxy()
|
||||||
|
.LoadFromMemory(
|
||||||
|
BuildRoutes(apexDomain, serviceMap),
|
||||||
|
BuildClusters(serviceMap));
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.MapDorkNetServiceDefaults();
|
||||||
|
|
||||||
|
var publicHosts = BuildPublicHosts(apexDomain);
|
||||||
|
app.Use(async (ctx, next) =>
|
||||||
|
{
|
||||||
|
if (DorkNetRouteOwnership.IsInfrastructurePath(ctx.Request.Path.Value ?? "/")
|
||||||
|
|| IsAllowedPublicHost(ctx.Request.Host.Host, publicHosts))
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||||
|
await ctx.Response.WriteAsync("Unknown DorkNet host.");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/internal/services", (IOptions<DorkNetServiceMapOptions> options) =>
|
||||||
|
Results.Ok(options.Value.Endpoints()));
|
||||||
|
app.MapGet("/internal/routes", () =>
|
||||||
|
Results.Ok(new
|
||||||
|
{
|
||||||
|
ApexDomain = apexDomain,
|
||||||
|
Routes = BuildRoutes(apexDomain, serviceMap).Select(r => new
|
||||||
|
{
|
||||||
|
r.RouteId,
|
||||||
|
r.ClusterId,
|
||||||
|
r.Match.Hosts,
|
||||||
|
r.Match.Path,
|
||||||
|
r.Order,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.MapGet("/internal/services/health", async (
|
||||||
|
IOptions<DorkNetServiceMapOptions> options,
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var client = httpClientFactory.CreateClient();
|
||||||
|
var probes = new List<ServiceProbeResponse>();
|
||||||
|
|
||||||
|
foreach (var endpoint in options.Value.Endpoints())
|
||||||
|
{
|
||||||
|
probes.Add(await ProbeServiceAsync(client, endpoint, ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok(probes);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapReverseProxy();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
static async Task<ServiceProbeResponse> ProbeServiceAsync(
|
||||||
|
HttpClient client,
|
||||||
|
DorkNetServiceEndpoint endpoint,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var checkedAt = DateTimeOffset.UtcNow;
|
||||||
|
var baseUrl = endpoint.BaseUrl.TrimEnd('/');
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await client.GetAsync($"{baseUrl}/internal/healthz", ct);
|
||||||
|
return new ServiceProbeResponse(
|
||||||
|
endpoint.Name,
|
||||||
|
endpoint.BaseUrl,
|
||||||
|
response.IsSuccessStatusCode ? "ok" : "unhealthy",
|
||||||
|
(int)response.StatusCode,
|
||||||
|
null,
|
||||||
|
checkedAt);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
|
||||||
|
{
|
||||||
|
return new ServiceProbeResponse(
|
||||||
|
endpoint.Name,
|
||||||
|
endpoint.BaseUrl,
|
||||||
|
"unreachable",
|
||||||
|
null,
|
||||||
|
ex.Message,
|
||||||
|
checkedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static IReadOnlyList<RouteConfig> BuildRoutes(
|
||||||
|
string apexDomain,
|
||||||
|
DorkNetServiceMapOptions services)
|
||||||
|
{
|
||||||
|
var routes = new List<RouteConfig>();
|
||||||
|
var order = 0;
|
||||||
|
var webGroup = DorkNetRouteOwnership.RouteGroups
|
||||||
|
.First(group => group.ServiceName == ServiceNames.Web);
|
||||||
|
|
||||||
|
AddHostRoute(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
webGroup.ServiceName,
|
||||||
|
BaseUrlFor(services, webGroup.ServiceName),
|
||||||
|
"web-admin-host",
|
||||||
|
[SubdomainHost("admin", apexDomain)]);
|
||||||
|
|
||||||
|
AddHostRoute(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
webGroup.ServiceName,
|
||||||
|
BaseUrlFor(services, webGroup.ServiceName),
|
||||||
|
"web-feed-host",
|
||||||
|
[SubdomainHost("feed", apexDomain)]);
|
||||||
|
|
||||||
|
AddHostPathRoutes(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
webGroup.ServiceName,
|
||||||
|
BaseUrlFor(services, webGroup.ServiceName),
|
||||||
|
WebBrowserHosts(apexDomain),
|
||||||
|
[
|
||||||
|
"/admin",
|
||||||
|
"/assets",
|
||||||
|
"/brand",
|
||||||
|
"/feed",
|
||||||
|
"/join",
|
||||||
|
"/p",
|
||||||
|
"/photo",
|
||||||
|
"/players",
|
||||||
|
"/rooms",
|
||||||
|
"/u",
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach (var group in DorkNetRouteOwnership.RouteGroups.Where(group => group.ServiceName != ServiceNames.Web))
|
||||||
|
{
|
||||||
|
AddHostRoute(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
group.ServiceName,
|
||||||
|
BaseUrlFor(services, group.ServiceName),
|
||||||
|
$"{group.ServiceName}-hosts",
|
||||||
|
group.HostSubdomains.Select(subdomain => SubdomainHost(subdomain, apexDomain)).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in DorkNetRouteOwnership.RouteGroups)
|
||||||
|
{
|
||||||
|
AddPathRoutes(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
group.ServiceName,
|
||||||
|
BaseUrlFor(services, group.ServiceName),
|
||||||
|
group.PathPrefixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddHostRoute(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
webGroup.ServiceName,
|
||||||
|
BaseUrlFor(services, webGroup.ServiceName),
|
||||||
|
"web-hosts",
|
||||||
|
BuildWebHosts(webGroup, apexDomain));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(services.Monolith))
|
||||||
|
{
|
||||||
|
routes.Add(Route(ServiceNames.Monolith, "monolith-fallback", "/{**catch-all}", order++));
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static IReadOnlyList<ClusterConfig> BuildClusters(DorkNetServiceMapOptions services)
|
||||||
|
{
|
||||||
|
var clusters = new List<ClusterConfig>();
|
||||||
|
foreach (var endpoint in services.Endpoints())
|
||||||
|
{
|
||||||
|
AddCluster(clusters, endpoint.Name, endpoint.BaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clusters;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string[] WebBrowserHosts(string apexDomain)
|
||||||
|
{
|
||||||
|
return [apexDomain, SubdomainHost("www", apexDomain)];
|
||||||
|
}
|
||||||
|
|
||||||
|
static HashSet<string> BuildPublicHosts(string apexDomain)
|
||||||
|
{
|
||||||
|
var hosts = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
apexDomain,
|
||||||
|
"localhost",
|
||||||
|
"127.0.0.1",
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var subdomain in DorkNetRouteOwnership.PublicSubdomains)
|
||||||
|
{
|
||||||
|
hosts.Add(SubdomainHost(subdomain, apexDomain));
|
||||||
|
}
|
||||||
|
|
||||||
|
return hosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsAllowedPublicHost(string? host, HashSet<string> publicHosts)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(host))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return publicHosts.Contains(host.Split(':', 2)[0].Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AddHostRoute(
|
||||||
|
List<RouteConfig> routes,
|
||||||
|
ref int order,
|
||||||
|
string serviceName,
|
||||||
|
string baseUrl,
|
||||||
|
string routeId,
|
||||||
|
string[] hosts)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(baseUrl) || hosts.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
routes.Add(Route(
|
||||||
|
serviceName,
|
||||||
|
routeId,
|
||||||
|
"/{**catch-all}",
|
||||||
|
order++,
|
||||||
|
hosts));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AddPathRoutes(
|
||||||
|
List<RouteConfig> routes,
|
||||||
|
ref int order,
|
||||||
|
string serviceName,
|
||||||
|
string baseUrl,
|
||||||
|
IEnumerable<string> paths)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var path in paths)
|
||||||
|
{
|
||||||
|
var routeId = $"{serviceName}-{path.Trim('/').Replace('/', '-').Replace("{**catch-all}", "all")}";
|
||||||
|
var exactPath = NormalizeRoutePrefix(path);
|
||||||
|
routes.Add(Route(serviceName, $"{routeId}-exact", exactPath, order++));
|
||||||
|
routes.Add(Route(serviceName, routeId, $"{exactPath}/{{**catch-all}}", order++));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AddHostPathRoutes(
|
||||||
|
List<RouteConfig> routes,
|
||||||
|
ref int order,
|
||||||
|
string serviceName,
|
||||||
|
string baseUrl,
|
||||||
|
string[] hosts,
|
||||||
|
IEnumerable<string> paths)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(baseUrl) || hosts.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var path in paths)
|
||||||
|
{
|
||||||
|
var routeId = $"{serviceName}-browser-{path.Trim('/').Replace('/', '-').Replace("{**catch-all}", "all")}";
|
||||||
|
var exactPath = NormalizeRoutePrefix(path);
|
||||||
|
routes.Add(Route(serviceName, $"{routeId}-exact", exactPath, order++, hosts));
|
||||||
|
routes.Add(Route(serviceName, routeId, $"{exactPath}/{{**catch-all}}", order++, hosts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string BaseUrlFor(DorkNetServiceMapOptions services, string serviceName)
|
||||||
|
{
|
||||||
|
return serviceName switch
|
||||||
|
{
|
||||||
|
ServiceNames.Identity => services.Identity,
|
||||||
|
ServiceNames.Rooms => services.Rooms,
|
||||||
|
ServiceNames.Notify => services.Notify,
|
||||||
|
ServiceNames.Content => services.Content,
|
||||||
|
ServiceNames.Social => services.Social,
|
||||||
|
ServiceNames.Commerce => services.Commerce,
|
||||||
|
ServiceNames.Platform => services.Platform,
|
||||||
|
ServiceNames.Moderation => services.Moderation,
|
||||||
|
ServiceNames.Web => services.Web,
|
||||||
|
ServiceNames.Monolith => services.Monolith,
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static string[] BuildWebHosts(DorkNetRouteGroup group, string apexDomain)
|
||||||
|
{
|
||||||
|
var hosts = new List<string>();
|
||||||
|
if (group.IncludesApexHost)
|
||||||
|
{
|
||||||
|
hosts.Add(apexDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts.AddRange(group.HostSubdomains
|
||||||
|
.Where(subdomain => !string.Equals(subdomain, "admin", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(subdomain => SubdomainHost(subdomain, apexDomain)));
|
||||||
|
|
||||||
|
return hosts.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string SubdomainHost(string subdomain, string apexDomain)
|
||||||
|
{
|
||||||
|
return $"{subdomain}.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
static string NormalizeRoutePrefix(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || path == "/")
|
||||||
|
{
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (path[0] == '/' ? path : "/" + path).TrimEnd('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
static RouteConfig Route(
|
||||||
|
string clusterId,
|
||||||
|
string routeId,
|
||||||
|
string path,
|
||||||
|
int order,
|
||||||
|
string[]? hosts = null)
|
||||||
|
{
|
||||||
|
return new RouteConfig
|
||||||
|
{
|
||||||
|
RouteId = routeId,
|
||||||
|
ClusterId = clusterId,
|
||||||
|
Order = order,
|
||||||
|
Match = new RouteMatch
|
||||||
|
{
|
||||||
|
Hosts = hosts,
|
||||||
|
Path = path,
|
||||||
|
},
|
||||||
|
Transforms =
|
||||||
|
[
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["RequestHeaderOriginalHost"] = "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AddCluster(
|
||||||
|
List<ClusterConfig> clusters,
|
||||||
|
string serviceName,
|
||||||
|
string baseUrl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = baseUrl.EndsWith('/') ? baseUrl : baseUrl + "/";
|
||||||
|
clusters.Add(new ClusterConfig
|
||||||
|
{
|
||||||
|
ClusterId = serviceName,
|
||||||
|
Destinations = new Dictionary<string, DestinationConfig>
|
||||||
|
{
|
||||||
|
["primary"] = new() { Address = address },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"System.Net.Http.HttpClient": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DorkNet": {
|
||||||
|
"Services": {
|
||||||
|
"Identity": "http://localhost:8081",
|
||||||
|
"Rooms": "http://localhost:8082",
|
||||||
|
"Notify": "http://localhost:8083",
|
||||||
|
"Content": "http://localhost:8084",
|
||||||
|
"Social": "http://localhost:8085",
|
||||||
|
"Commerce": "http://localhost:8086",
|
||||||
|
"Platform": "http://localhost:8087",
|
||||||
|
"Moderation": "http://localhost:8088",
|
||||||
|
"Web": "http://localhost:8089",
|
||||||
|
"Monolith": "http://localhost:8090"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.HostFiltering;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace DorkNet.Server.Tests;
|
||||||
|
|
||||||
|
public sealed class DorkNetServerFactory : WebApplicationFactory<Program>
|
||||||
|
{
|
||||||
|
private const string JwtSecret = "endpoint-contract-secret-000000000000000000000000000000000000000000";
|
||||||
|
private readonly string _dataRoot;
|
||||||
|
|
||||||
|
public DorkNetServerFactory()
|
||||||
|
{
|
||||||
|
_dataRoot = Path.Combine(Path.GetTempPath(), "dorknet-server-tests", Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(_dataRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ApexDomain => "dork.test";
|
||||||
|
|
||||||
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
|
{
|
||||||
|
builder.UseEnvironment("Development");
|
||||||
|
builder.UseSetting("Domain:Apex", ApexDomain);
|
||||||
|
builder.ConfigureAppConfiguration((_, config) =>
|
||||||
|
{
|
||||||
|
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["AllowedHosts"] = "*",
|
||||||
|
["Database:Provider"] = "sqlite",
|
||||||
|
["Database:SqlitePath"] = Path.Combine(_dataRoot, "dorknet-test.db"),
|
||||||
|
["Domain:Apex"] = ApexDomain,
|
||||||
|
["Jwt:Secret"] = JwtSecret,
|
||||||
|
["S3:Endpoint"] = "",
|
||||||
|
["S3:AccessKey"] = "",
|
||||||
|
["S3:SecretKey"] = "",
|
||||||
|
["DorkNet:DefaultClientVersion"] = "december_2020_12_18",
|
||||||
|
["DorkNet:SupportedVersions:0"] = "december_2020_12_18",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
builder.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.PostConfigure<HostFilteringOptions>(options =>
|
||||||
|
{
|
||||||
|
options.AllowedHosts =
|
||||||
|
[
|
||||||
|
ApexDomain,
|
||||||
|
$"*.{ApexDomain}",
|
||||||
|
"localhost",
|
||||||
|
"127.0.0.1",
|
||||||
|
];
|
||||||
|
options.AllowEmptyHosts = true;
|
||||||
|
options.IncludeFailureMessage = true;
|
||||||
|
});
|
||||||
|
services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret)),
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = "https://api.rec.net/",
|
||||||
|
ValidateAudience = false,
|
||||||
|
ClockSkew = TimeSpan.Zero,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_dataRoot))
|
||||||
|
{
|
||||||
|
Directory.Delete(_dataRoot, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Test cleanup must not hide the original test failure.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
|
|
||||||
|
namespace DorkNet.Server.Tests;
|
||||||
|
|
||||||
|
public static partial class EndpointContractDiscovery
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<EndpointContract> Discover()
|
||||||
|
{
|
||||||
|
var serverAssembly = typeof(Program).Assembly;
|
||||||
|
var controllerBaseType = typeof(ControllerBase);
|
||||||
|
return serverAssembly.GetTypes()
|
||||||
|
.Where(t => !t.IsAbstract && controllerBaseType.IsAssignableFrom(t))
|
||||||
|
.SelectMany(DiscoverControllerRoutes)
|
||||||
|
.OrderBy(route => route.Method, StringComparer.Ordinal)
|
||||||
|
.ThenBy(route => route.SamplePath, StringComparer.Ordinal)
|
||||||
|
.ThenBy(route => route.Source, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<EndpointContract> DiscoverControllerRoutes(Type controller)
|
||||||
|
{
|
||||||
|
var controllerName = ControllerName(controller);
|
||||||
|
var controllerRouteTemplates = controller.GetCustomAttributes(inherit: true)
|
||||||
|
.OfType<IRouteTemplateProvider>()
|
||||||
|
.Select(a => a.Template)
|
||||||
|
.Where(t => !string.IsNullOrWhiteSpace(t))
|
||||||
|
.Cast<string>()
|
||||||
|
.DefaultIfEmpty("")
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var method in controller.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
|
||||||
|
{
|
||||||
|
var httpAttrs = method.GetCustomAttributes(inherit: true)
|
||||||
|
.OfType<HttpMethodAttribute>()
|
||||||
|
.ToArray();
|
||||||
|
if (httpAttrs.Length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var attr in httpAttrs)
|
||||||
|
{
|
||||||
|
var attrMethods = attr.HttpMethods.Any()
|
||||||
|
? attr.HttpMethods
|
||||||
|
: new[] { "ANY" };
|
||||||
|
|
||||||
|
foreach (var httpMethod in attrMethods)
|
||||||
|
{
|
||||||
|
foreach (var controllerTemplate in controllerRouteTemplates)
|
||||||
|
{
|
||||||
|
var template = CombineTemplates(controllerTemplate, attr.Template);
|
||||||
|
template = ReplaceRouteTokens(template, controllerName, method.Name);
|
||||||
|
yield return new EndpointContract(
|
||||||
|
httpMethod.ToUpperInvariant(),
|
||||||
|
template,
|
||||||
|
ToSamplePath(template),
|
||||||
|
$"{controller.FullName}.{method.Name}",
|
||||||
|
RequiresAuthorization(controller, method),
|
||||||
|
IsAdminEndpoint(controller, method),
|
||||||
|
ConsumesContentTypes(controller, method));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RequiresAuthorization(Type controller, MethodInfo method)
|
||||||
|
{
|
||||||
|
var attributes = controller.GetCustomAttributes(inherit: true)
|
||||||
|
.Concat(method.GetCustomAttributes(inherit: true))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (attributes.OfType<IAllowAnonymous>().Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes.OfType<IAuthorizeData>().Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAdminEndpoint(Type controller, MethodInfo method)
|
||||||
|
{
|
||||||
|
return controller.FullName?.Contains(".Controllers.Admin.", StringComparison.Ordinal) == true
|
||||||
|
|| method.Name.Contains("Admin", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] ConsumesContentTypes(Type controller, MethodInfo method)
|
||||||
|
{
|
||||||
|
var consumes = method.GetCustomAttributes(inherit: true)
|
||||||
|
.Concat(controller.GetCustomAttributes(inherit: true))
|
||||||
|
.OfType<ConsumesAttribute>()
|
||||||
|
.SelectMany(a => a.ContentTypes)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return consumes.Length == 0
|
||||||
|
? ["application/json"]
|
||||||
|
: consumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ControllerName(Type controller)
|
||||||
|
{
|
||||||
|
const string suffix = "Controller";
|
||||||
|
return controller.Name.EndsWith(suffix, StringComparison.Ordinal)
|
||||||
|
? controller.Name[..^suffix.Length]
|
||||||
|
: controller.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CombineTemplates(string? controllerTemplate, string? actionTemplate)
|
||||||
|
{
|
||||||
|
controllerTemplate ??= "";
|
||||||
|
actionTemplate ??= "";
|
||||||
|
|
||||||
|
if (actionTemplate.StartsWith("~/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return "/" + actionTemplate[2..].TrimStart('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionTemplate.StartsWith("/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return "/" + actionTemplate.TrimStart('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
var left = controllerTemplate.Trim('/');
|
||||||
|
var right = actionTemplate.Trim('/');
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(left) && string.IsNullOrEmpty(right))
|
||||||
|
{
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(left))
|
||||||
|
{
|
||||||
|
return "/" + right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(right))
|
||||||
|
{
|
||||||
|
return "/" + left;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/" + left + "/" + right;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReplaceRouteTokens(string template, string controllerName, string actionName)
|
||||||
|
{
|
||||||
|
return template
|
||||||
|
.Replace("[controller]", controllerName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("[action]", actionName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToSamplePath(string template)
|
||||||
|
{
|
||||||
|
var path = RouteParameterRegex().Replace(template, match =>
|
||||||
|
{
|
||||||
|
var name = match.Groups["name"].Value.ToLowerInvariant();
|
||||||
|
var constraint = match.Groups["constraint"].Value.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (name.Contains("playerid") || name.Contains("accountid") || name == "id")
|
||||||
|
{
|
||||||
|
return EndpointContract.TestPlayerToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraint.Contains("long") || constraint.Contains("int") || name.EndsWith("id"))
|
||||||
|
{
|
||||||
|
return "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraint.Contains("bool"))
|
||||||
|
{
|
||||||
|
return "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraint.Contains("guid"))
|
||||||
|
{
|
||||||
|
return "00000000-0000-0000-0000-000000000001";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.Contains("roomname"))
|
||||||
|
{
|
||||||
|
return "DormRoom";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.Contains("locale"))
|
||||||
|
{
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "sample";
|
||||||
|
});
|
||||||
|
|
||||||
|
path = Regex.Replace(path, "/+", "/");
|
||||||
|
return path.StartsWith('/') ? path : "/" + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\{[*]?(?<name>[^}:=\?]+)(?<constraint>:[^}=\?]+)?(?<optional>\?)?(?<default>=[^}]+)?\}")]
|
||||||
|
private static partial Regex RouteParameterRegex();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record EndpointContract(
|
||||||
|
string Method,
|
||||||
|
string Template,
|
||||||
|
string SamplePath,
|
||||||
|
string Source,
|
||||||
|
bool RequiresAuthorization,
|
||||||
|
bool IsAdminEndpoint,
|
||||||
|
string[] Consumes)
|
||||||
|
{
|
||||||
|
public const string TestPlayerToken = "__test_player_id__";
|
||||||
|
|
||||||
|
public string PathFor(GameClientSession session)
|
||||||
|
{
|
||||||
|
return SamplePath.Replace(
|
||||||
|
TestPlayerToken,
|
||||||
|
session.PlayerId.ToString(),
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace DorkNet.Server.Tests;
|
||||||
|
|
||||||
|
public sealed class GameClientEndpointContractTests :
|
||||||
|
IClassFixture<DorkNetServerFactory>
|
||||||
|
{
|
||||||
|
private readonly DorkNetServerFactory _factory;
|
||||||
|
|
||||||
|
public GameClientEndpointContractTests(DorkNetServerFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Game_client_contract_probe_reaches_every_controller_route()
|
||||||
|
{
|
||||||
|
var routes = EndpointContractDiscovery.Discover();
|
||||||
|
Assert.NotEmpty(routes);
|
||||||
|
|
||||||
|
using var client = _factory.CreateClient(new()
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = false,
|
||||||
|
});
|
||||||
|
client.BaseAddress = new Uri($"http://api.{_factory.ApexDomain}");
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(20);
|
||||||
|
|
||||||
|
var session = await GameClientSessionFactory.CreateAsync(client, _factory.ApexDomain);
|
||||||
|
var failures = new List<string>();
|
||||||
|
|
||||||
|
foreach (var route in routes)
|
||||||
|
{
|
||||||
|
var result = await ProbeAsync(client, route, session, _factory.ApexDomain);
|
||||||
|
if (!result.Passed)
|
||||||
|
{
|
||||||
|
failures.Add(
|
||||||
|
$"{route.Method,-6} {result.Host}{result.Path} {route.Source} expected {result.Expected}, got {result.Observed}"
|
||||||
|
+ (string.IsNullOrWhiteSpace(result.ResponseExcerpt) ? "" : $" body={result.ResponseExcerpt}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
failures.Count == 0,
|
||||||
|
$"Endpoint contract failures ({failures.Count}/{routes.Count}):{Environment.NewLine}"
|
||||||
|
+ string.Join(Environment.NewLine, failures));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<EndpointProbeResult> ProbeAsync(
|
||||||
|
HttpClient client,
|
||||||
|
EndpointContract route,
|
||||||
|
GameClientSession session,
|
||||||
|
string apexDomain)
|
||||||
|
{
|
||||||
|
var path = route.PathFor(session);
|
||||||
|
path = AddClientQuery(path, route, session);
|
||||||
|
var host = HostFor(path, apexDomain);
|
||||||
|
using var request = new HttpRequestMessage(
|
||||||
|
ToHttpMethod(route.Method),
|
||||||
|
new Uri($"http://{host}{path}"));
|
||||||
|
request.Headers.Accept.ParseAdd("application/json");
|
||||||
|
request.Headers.UserAgent.ParseAdd("RecRoom/2020.12.18");
|
||||||
|
request.Headers.TryAddWithoutValidation("X-DorkNet-Version", "december_2020_12_18");
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", session.AccessToken);
|
||||||
|
|
||||||
|
if (NeedsRequestBody(request.Method))
|
||||||
|
{
|
||||||
|
request.Content = CreateContent(route, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await client.SendAsync(request);
|
||||||
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
var statusCode = (int)response.StatusCode;
|
||||||
|
var passed = ResponseMatchesContract(route, response.StatusCode);
|
||||||
|
return new EndpointProbeResult(
|
||||||
|
route.Method,
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
ExpectedContract(route),
|
||||||
|
statusCode,
|
||||||
|
null,
|
||||||
|
Excerpt(responseText),
|
||||||
|
passed);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
|
||||||
|
{
|
||||||
|
return new EndpointProbeResult(
|
||||||
|
route.Method,
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
ExpectedContract(route),
|
||||||
|
null,
|
||||||
|
ex.Message,
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ResponseMatchesContract(
|
||||||
|
EndpointContract route,
|
||||||
|
HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
if ((int)statusCode >= 500)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode is HttpStatusCode.MethodNotAllowed
|
||||||
|
or HttpStatusCode.NotImplemented
|
||||||
|
or HttpStatusCode.UpgradeRequired)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.RequiresAuthorization
|
||||||
|
&& statusCode is HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExpectedContract(EndpointContract route)
|
||||||
|
{
|
||||||
|
if (route.IsAdminEndpoint)
|
||||||
|
{
|
||||||
|
return "admin endpoint may reject non-admin test player, but must not 5xx/405/426";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.RequiresAuthorization)
|
||||||
|
{
|
||||||
|
return "authenticated test player should reach the action without 401/5xx/405/426; 403 is valid for owner/admin-only resources";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "game-client request should not 5xx/405/426";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpMethod ToHttpMethod(string method)
|
||||||
|
{
|
||||||
|
return string.Equals(method, "ANY", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? HttpMethod.Get
|
||||||
|
: new HttpMethod(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool NeedsRequestBody(HttpMethod method)
|
||||||
|
{
|
||||||
|
return method == HttpMethod.Post
|
||||||
|
|| method == HttpMethod.Put
|
||||||
|
|| method.Method.Equals("PATCH", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpContent CreateContent(
|
||||||
|
EndpointContract route,
|
||||||
|
GameClientSession session)
|
||||||
|
{
|
||||||
|
var consumes = route.Consumes.Select(c => c.ToLowerInvariant()).ToArray();
|
||||||
|
if (consumes.Any(c => c.Contains("application/x-www-form-urlencoded")))
|
||||||
|
{
|
||||||
|
return new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["account_id"] = session.PlayerId.ToString(),
|
||||||
|
["AccountId"] = session.PlayerId.ToString(),
|
||||||
|
["clubId"] = "1",
|
||||||
|
["ClubId"] = "1",
|
||||||
|
["description"] = "Endpoint contract description",
|
||||||
|
["deviceId"] = session.DeviceId,
|
||||||
|
["grant_type"] = "password",
|
||||||
|
["image"] = "endpoint-contract.png",
|
||||||
|
["imageName"] = "endpoint-contract.png",
|
||||||
|
["fileName"] = "endpoint-contract.png",
|
||||||
|
["message"] = "Endpoint contract message",
|
||||||
|
["name"] = "EndpointContract",
|
||||||
|
["newPassword"] = "endpoint-contract-password",
|
||||||
|
["oldPassword"] = "",
|
||||||
|
["password"] = "endpoint-contract-password",
|
||||||
|
["ping"] = "42",
|
||||||
|
["platformId"] = session.PlatformId,
|
||||||
|
["playerId"] = session.PlayerId.ToString(),
|
||||||
|
["playerIds"] = session.PlayerId.ToString(),
|
||||||
|
["query"] = "DormRoom",
|
||||||
|
["region"] = "us",
|
||||||
|
["targetPlayerId"] = session.PlayerId.ToString(),
|
||||||
|
["text"] = "Endpoint contract message",
|
||||||
|
["username"] = "EndpointContract",
|
||||||
|
["device_id"] = session.DeviceId,
|
||||||
|
["platform"] = "0",
|
||||||
|
["platform_id"] = session.PlatformId,
|
||||||
|
["Ids"] = session.PlayerId.ToString(),
|
||||||
|
["PlayerIds"] = session.PlayerId.ToString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consumes.Any(c => c.Contains("multipart/form-data")))
|
||||||
|
{
|
||||||
|
var content = new MultipartFormDataContent();
|
||||||
|
content.Add(new StringContent("endpoint-contract"), "name");
|
||||||
|
content.Add(new StringContent(session.PlayerId.ToString()), "playerId");
|
||||||
|
content.Add(new StringContent("endpoint-contract.png"), "imageName");
|
||||||
|
content.Add(new StringContent("endpoint-contract.png"), "fileName");
|
||||||
|
content.Add(new ByteArrayContent([]), "file", "endpoint-contract.bin");
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consumes.Any(c => c.Contains("text/plain")))
|
||||||
|
{
|
||||||
|
return new StringContent("", Encoding.UTF8, "text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
PlayerId = session.PlayerId,
|
||||||
|
AccountId = session.PlayerId,
|
||||||
|
ClubId = 1,
|
||||||
|
Description = "Endpoint contract description",
|
||||||
|
DeviceId = session.DeviceId,
|
||||||
|
FileName = "endpoint-contract.png",
|
||||||
|
Id = session.PlayerId,
|
||||||
|
Image = "endpoint-contract.png",
|
||||||
|
ImageName = "endpoint-contract.png",
|
||||||
|
Message = "Endpoint contract message",
|
||||||
|
Platform = 0,
|
||||||
|
PlatformId = session.PlatformId,
|
||||||
|
PlayerIds = new[] { session.PlayerId },
|
||||||
|
Name = "EndpointContract",
|
||||||
|
Password = "endpoint-contract-password",
|
||||||
|
Query = "DormRoom",
|
||||||
|
RoomName = "DormRoom",
|
||||||
|
TargetPlayerId = session.PlayerId,
|
||||||
|
Text = "Endpoint contract message",
|
||||||
|
});
|
||||||
|
return new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string AddClientQuery(
|
||||||
|
string path,
|
||||||
|
EndpointContract route,
|
||||||
|
GameClientSession session)
|
||||||
|
{
|
||||||
|
var separator = path.Contains('?', StringComparison.Ordinal) ? "&" : "?";
|
||||||
|
var lower = path.ToLowerInvariant();
|
||||||
|
var query = new List<string>();
|
||||||
|
|
||||||
|
if (lower.Contains("search", StringComparison.Ordinal)
|
||||||
|
|| lower.Contains("query", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
query.Add("query=EndpointContract");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower == "/members/bulk")
|
||||||
|
{
|
||||||
|
query.Add("clubId=1");
|
||||||
|
query.Add($"playerIds={session.PlayerId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower == "/player/photonregionpings")
|
||||||
|
{
|
||||||
|
query.Add("region=us");
|
||||||
|
query.Add("ping=42");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.Source.Contains("CachedLogins", StringComparison.Ordinal)
|
||||||
|
|| lower.Contains("platformlogin/cached", StringComparison.Ordinal)
|
||||||
|
|| lower.Contains("savedlogins", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
query.Add("platform=0");
|
||||||
|
query.Add($"platformId={Uri.EscapeDataString(session.PlatformId)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.Source.Contains("Bulk", StringComparison.Ordinal)
|
||||||
|
&& !query.Any(q => q.StartsWith("Ids=", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
query.Add($"Ids={session.PlayerId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.Count == 0)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path + separator + string.Join("&", query);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HostFor(string path, string apexDomain)
|
||||||
|
{
|
||||||
|
var lower = path.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (lower.StartsWith("/connect/")
|
||||||
|
|| lower.StartsWith("/.well-known/")
|
||||||
|
|| lower.StartsWith("/eac/")
|
||||||
|
|| lower.StartsWith("/cachedlogin/")
|
||||||
|
|| lower.StartsWith("/photon/")
|
||||||
|
|| lower.StartsWith("/api/platformlogin/")
|
||||||
|
|| lower.StartsWith("/api/version")
|
||||||
|
|| lower.StartsWith("/api/auth/"))
|
||||||
|
{
|
||||||
|
return $"auth.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/account/"))
|
||||||
|
{
|
||||||
|
return $"accounts.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/roomserver/")
|
||||||
|
|| lower.StartsWith("/rooms")
|
||||||
|
|| lower.StartsWith("/playlists")
|
||||||
|
|| lower.StartsWith("/featuredrooms")
|
||||||
|
|| lower.StartsWith("/hot_rooms")
|
||||||
|
|| lower.StartsWith("/search_rooms")
|
||||||
|
|| lower.StartsWith("/api/rooms")
|
||||||
|
|| lower.StartsWith("/api/roomkeys")
|
||||||
|
|| lower.StartsWith("/api/gamesessions")
|
||||||
|
|| lower.StartsWith("/api/curatedroomplaylists"))
|
||||||
|
{
|
||||||
|
return $"rooms.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/v1/find")
|
||||||
|
|| lower.StartsWith("/v1/join")
|
||||||
|
|| lower.StartsWith("/v1/leave")
|
||||||
|
|| lower.StartsWith("/v1/session")
|
||||||
|
|| lower.StartsWith("/roominstance/")
|
||||||
|
|| lower.StartsWith("/goto/")
|
||||||
|
|| lower.StartsWith("/player/"))
|
||||||
|
{
|
||||||
|
return $"match.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/thread"))
|
||||||
|
{
|
||||||
|
return $"chat.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/club")
|
||||||
|
|| lower.StartsWith("/announcements/")
|
||||||
|
|| lower.StartsWith("/subscription/")
|
||||||
|
|| lower.StartsWith("/members/")
|
||||||
|
|| lower.StartsWith("/api/clubreporting/"))
|
||||||
|
{
|
||||||
|
return $"clubs.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/api/storefronts/")
|
||||||
|
|| lower.StartsWith("/api/purchase/")
|
||||||
|
|| lower.StartsWith("/api/subscription/"))
|
||||||
|
{
|
||||||
|
return $"commerce.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/hub/") || lower.StartsWith("/api/notification/"))
|
||||||
|
{
|
||||||
|
return $"notify.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/v1/regions"))
|
||||||
|
{
|
||||||
|
return $"geo.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/v1/services") || lower.StartsWith("/v1/ping"))
|
||||||
|
{
|
||||||
|
return $"ns.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/upload"))
|
||||||
|
{
|
||||||
|
return $"storage.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/img/") || lower.StartsWith("/room/") || lower.StartsWith("/data/"))
|
||||||
|
{
|
||||||
|
return $"cdn.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/api/admin/"))
|
||||||
|
{
|
||||||
|
return $"admin.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"api.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Excerpt(string? body)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var compact = body.ReplaceLineEndings(" ").Trim();
|
||||||
|
return compact.Length <= 240 ? compact : compact[..240] + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record EndpointProbeResult(
|
||||||
|
string Method,
|
||||||
|
string Host,
|
||||||
|
string Path,
|
||||||
|
string Expected,
|
||||||
|
int? StatusCode,
|
||||||
|
string? Error,
|
||||||
|
string? ResponseExcerpt,
|
||||||
|
bool Passed)
|
||||||
|
{
|
||||||
|
public string Observed => StatusCode is int code ? code.ToString() : Error ?? "no response";
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace DorkNet.Server.Tests;
|
||||||
|
|
||||||
|
public sealed record GameClientSession(
|
||||||
|
long PlayerId,
|
||||||
|
string AccessToken,
|
||||||
|
string DeviceId,
|
||||||
|
string PlatformId);
|
||||||
|
|
||||||
|
public static class GameClientSessionFactory
|
||||||
|
{
|
||||||
|
public static async Task<GameClientSession> CreateAsync(
|
||||||
|
HttpClient client,
|
||||||
|
string apexDomain)
|
||||||
|
{
|
||||||
|
var deviceId = $"endpoint-contract-device-{Guid.NewGuid():N}";
|
||||||
|
var platformId = $"endpoint-contract-platform-{Guid.NewGuid():N}";
|
||||||
|
var tokenRequest = new HttpRequestMessage(
|
||||||
|
HttpMethod.Post,
|
||||||
|
new Uri($"http://auth.{apexDomain}/connect/token"));
|
||||||
|
tokenRequest.Headers.Accept.ParseAdd("application/json");
|
||||||
|
tokenRequest.Headers.UserAgent.ParseAdd("RecRoom/2020.12.18");
|
||||||
|
tokenRequest.Headers.TryAddWithoutValidation("X-DorkNet-Version", "december_2020_12_18");
|
||||||
|
tokenRequest.Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["grant_type"] = "password",
|
||||||
|
["username"] = "EndpointContract",
|
||||||
|
["device_id"] = deviceId,
|
||||||
|
["platform"] = "0",
|
||||||
|
["platform_id"] = platformId,
|
||||||
|
});
|
||||||
|
|
||||||
|
using var tokenResponse = await client.SendAsync(tokenRequest);
|
||||||
|
if (!tokenResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var body = await tokenResponse.Content.ReadAsStringAsync();
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Test player token request failed with {(int)tokenResponse.StatusCode} {tokenResponse.StatusCode}: {body}");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var tokenJson = await JsonDocument.ParseAsync(
|
||||||
|
await tokenResponse.Content.ReadAsStreamAsync());
|
||||||
|
var accessToken = tokenJson.RootElement.GetProperty("access_token").GetString()
|
||||||
|
?? throw new InvalidOperationException("Token response did not include access_token.");
|
||||||
|
|
||||||
|
var userInfoRequest = new HttpRequestMessage(
|
||||||
|
HttpMethod.Get,
|
||||||
|
new Uri($"http://auth.{apexDomain}/connect/userinfo"));
|
||||||
|
userInfoRequest.Headers.Accept.ParseAdd("application/json");
|
||||||
|
userInfoRequest.Headers.UserAgent.ParseAdd("RecRoom/2020.12.18");
|
||||||
|
userInfoRequest.Headers.TryAddWithoutValidation("X-DorkNet-Version", "december_2020_12_18");
|
||||||
|
userInfoRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||||
|
|
||||||
|
using var userInfoResponse = await client.SendAsync(userInfoRequest);
|
||||||
|
userInfoResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
using var userInfoJson = await JsonDocument.ParseAsync(
|
||||||
|
await userInfoResponse.Content.ReadAsStreamAsync());
|
||||||
|
var playerId = long.Parse(userInfoJson.RootElement.GetProperty("sub").GetString()!);
|
||||||
|
|
||||||
|
return new GameClientSession(playerId, accessToken, deviceId, platformId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using DorkNet.Server.Compat;
|
||||||
|
using DorkNet.Server.Controllers.Auth;
|
||||||
|
using DorkNet.Server.Versions.Late2020;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
|
namespace DorkNet.Server.Tests;
|
||||||
|
|
||||||
|
public sealed class VersionCheckControllerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void December_2020_build_is_valid_with_minimal_service_config()
|
||||||
|
{
|
||||||
|
var controller = CreateController();
|
||||||
|
|
||||||
|
var result = Assert.IsType<OkObjectResult>(controller.Check("20201210", "0"));
|
||||||
|
|
||||||
|
Assert.Equal(0, ReadVersionStatus(result.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Unknown_build_still_requires_update()
|
||||||
|
{
|
||||||
|
var controller = CreateController();
|
||||||
|
|
||||||
|
var result = Assert.IsType<OkObjectResult>(controller.Check("unknown", "0"));
|
||||||
|
|
||||||
|
Assert.Equal(2, ReadVersionStatus(result.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VersionCheckController CreateController()
|
||||||
|
{
|
||||||
|
var config = new ConfigurationBuilder().Build();
|
||||||
|
var registry = new VersionRegistry(
|
||||||
|
[new Late2020VersionPlugin()],
|
||||||
|
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"december_2020_12_18",
|
||||||
|
},
|
||||||
|
"december_2020_12_18");
|
||||||
|
|
||||||
|
return new VersionCheckController(
|
||||||
|
config,
|
||||||
|
registry,
|
||||||
|
NullLogger<VersionCheckController>.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadVersionStatus(object? value)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(value);
|
||||||
|
using var document = JsonDocument.Parse(json);
|
||||||
|
return document.RootElement.GetProperty("VersionStatus").GetInt32();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -201,12 +201,12 @@ public class AccountApiController(
|
|||||||
|
|
||||||
[HttpPost("api/account/v1/profileimage")]
|
[HttpPost("api/account/v1/profileimage")]
|
||||||
[HttpPost("api/account/me/profileimage")]
|
[HttpPost("api/account/me/profileimage")]
|
||||||
[HttpPost("account/me/profileimage")]
|
[HttpPost("account/me/profileimage", Order = 10)]
|
||||||
[HttpPost("profileimage")]
|
[HttpPost("profileimage", Order = 10)]
|
||||||
[HttpPut("api/account/v1/profileimage")]
|
[HttpPut("api/account/v1/profileimage")]
|
||||||
[HttpPut("api/account/me/profileimage")]
|
[HttpPut("api/account/me/profileimage")]
|
||||||
[HttpPut("account/me/profileimage")]
|
[HttpPut("account/me/profileimage", Order = 10)]
|
||||||
[HttpPut("profileimage")]
|
[HttpPut("profileimage", Order = 10)]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
public async Task<IActionResult> SetProfileImage(
|
public async Task<IActionResult> SetProfileImage(
|
||||||
[FromForm(Name = "imageName")] string? imageName,
|
[FromForm(Name = "imageName")] string? imageName,
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ public class ServiceUrlsController(DomainConfig domain) : ControllerBase
|
|||||||
{
|
{
|
||||||
[HttpGet("services")]
|
[HttpGet("services")]
|
||||||
public ActionResult<Dictionary<string, string>> Get()
|
public ActionResult<Dictionary<string, string>> Get()
|
||||||
=> Ok(ConfigService.BuildServiceUrlMap(domain.Apex));
|
=> Ok(ConfigService.BuildServiceUrlMap(domain));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class NotificationController(DomainConfig domain) : ControllerBase
|
|||||||
public IActionResult Connect() => StatusCode(StatusCodes.Status410Gone, new
|
public IActionResult Connect() => StatusCode(StatusCodes.Status410Gone, new
|
||||||
{
|
{
|
||||||
error = "use_signalr_hub",
|
error = "use_signalr_hub",
|
||||||
hint = $"connect to {domain.Sub("notify")}/hub/v1 instead",
|
hint = $"connect to {domain.Url("notify", "/hub/v1")} instead",
|
||||||
});
|
});
|
||||||
|
|
||||||
/// <summary>GET <c>api/notification/hub/v1</c> — returns the
|
/// <summary>GET <c>api/notification/hub/v1</c> — returns the
|
||||||
@@ -27,7 +27,7 @@ public class NotificationController(DomainConfig domain) : ControllerBase
|
|||||||
[HttpGet("/api/notification/hub/v1")]
|
[HttpGet("/api/notification/hub/v1")]
|
||||||
public IActionResult HubInfo() => Ok(new
|
public IActionResult HubInfo() => Ok(new
|
||||||
{
|
{
|
||||||
Url = $"https://{domain.Sub("notify")}/hub/v1",
|
Url = domain.Url("notify", "/hub/v1"),
|
||||||
Protocol = "signalr",
|
Protocol = "signalr",
|
||||||
Version = 1,
|
Version = 1,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ public class PageviewController(ILogger<PageviewController> logger) : Controller
|
|||||||
/// intentionally return an empty url + a 1-hour freshness — the
|
/// intentionally return an empty url + a 1-hour freshness — the
|
||||||
/// private server has no in-app deep-link system, so there's
|
/// private server has no in-app deep-link system, so there's
|
||||||
/// nothing to navigate to and no reason to be polled more often.</summary>
|
/// nothing to navigate to and no reason to be polled more often.</summary>
|
||||||
[HttpPost("pageview/consume")]
|
[HttpPost("pageview/consume", Order = 10)]
|
||||||
[HttpPost("/pageview/consume")]
|
[HttpPost("/pageview/consume")]
|
||||||
[HttpGet("pageview/consume")]
|
[HttpGet("pageview/consume", Order = 10)]
|
||||||
[HttpGet("/pageview/consume")]
|
[HttpGet("/pageview/consume")]
|
||||||
public IActionResult Consume()
|
public IActionResult Consume()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class AuthAccountController(DorkNetDbContext db) : ControllerBase
|
|||||||
/// <c>ExpectPrimitiveResponse<bool></c> so the body must be
|
/// <c>ExpectPrimitiveResponse<bool></c> so the body must be
|
||||||
/// the literal JSON <c>true</c> or <c>false</c>.</summary>
|
/// the literal JSON <c>true</c> or <c>false</c>.</summary>
|
||||||
[HttpGet("/account/me/haspassword")]
|
[HttpGet("/account/me/haspassword")]
|
||||||
[HttpGet("/api/platformlogin/haspassword")]
|
[HttpGet("/api/platformlogin/haspassword", Order = 10)]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> HasPassword()
|
public async Task<IActionResult> HasPassword()
|
||||||
{
|
{
|
||||||
@@ -40,8 +40,8 @@ public class AuthAccountController(DorkNetDbContext db) : ControllerBase
|
|||||||
/// one; first-time creation skips that check. Form-encoded body.</summary>
|
/// one; first-time creation skips that check. Form-encoded body.</summary>
|
||||||
[HttpPost("/account/me/changepassword")]
|
[HttpPost("/account/me/changepassword")]
|
||||||
[HttpPost("/account/me/createpassword")]
|
[HttpPost("/account/me/createpassword")]
|
||||||
[HttpPost("/api/platformlogin/password")]
|
[HttpPost("/api/platformlogin/password", Order = 10)]
|
||||||
[HttpPut("/api/platformlogin/password")]
|
[HttpPut("/api/platformlogin/password", Order = 10)]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> ChangePassword(
|
public async Task<IActionResult> ChangePassword(
|
||||||
@@ -76,7 +76,7 @@ public class AuthAccountController(DorkNetDbContext db) : ControllerBase
|
|||||||
/// account from the admin tab and the user re-creates it via
|
/// account from the admin tab and the user re-creates it via
|
||||||
/// changepassword on next login. Anonymous calls 401.</summary>
|
/// changepassword on next login. Anonymous calls 401.</summary>
|
||||||
[HttpPost("/account/me/resetpassword")]
|
[HttpPost("/account/me/resetpassword")]
|
||||||
[HttpPost("/api/platformlogin/resetpassword")]
|
[HttpPost("/api/platformlogin/resetpassword", Order = 10)]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[AdminOnly]
|
[AdminOnly]
|
||||||
public async Task<IActionResult> AdminResetPassword(
|
public async Task<IActionResult> AdminResetPassword(
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ public class VersionCheckController(
|
|||||||
private const int VersionStatus_ValidForMenu = 1;
|
private const int VersionStatus_ValidForMenu = 1;
|
||||||
private const int VersionStatus_UpdateRequired = 2;
|
private const int VersionStatus_UpdateRequired = 2;
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> BuiltInBuildIdToVersionKey =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["20201210"] = "december_2020_12_18",
|
||||||
|
};
|
||||||
|
|
||||||
[HttpGet("/api/versioncheck/v4")]
|
[HttpGet("/api/versioncheck/v4")]
|
||||||
[HttpGet("/api/versioncheck/v3")]
|
[HttpGet("/api/versioncheck/v3")]
|
||||||
[HttpGet("/api/versioncheck/v2")]
|
[HttpGet("/api/versioncheck/v2")]
|
||||||
@@ -66,9 +72,17 @@ public class VersionCheckController(
|
|||||||
// }
|
// }
|
||||||
// Mapping ABSENT → unknown build → UpdateRequired.
|
// Mapping ABSENT → unknown build → UpdateRequired.
|
||||||
var mapSection = config.GetSection("DorkNet:BuildIdToVersionKey");
|
var mapSection = config.GetSection("DorkNet:BuildIdToVersionKey");
|
||||||
string? versionKey = !string.IsNullOrWhiteSpace(buildId)
|
string? versionKey = null;
|
||||||
? mapSection[buildId]
|
if (!string.IsNullOrWhiteSpace(buildId))
|
||||||
: null;
|
{
|
||||||
|
var normalizedBuildId = buildId.Trim();
|
||||||
|
versionKey = mapSection[normalizedBuildId];
|
||||||
|
if (string.IsNullOrWhiteSpace(versionKey)
|
||||||
|
&& BuiltInBuildIdToVersionKey.TryGetValue(normalizedBuildId, out var builtInVersionKey))
|
||||||
|
{
|
||||||
|
versionKey = builtInVersionKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool allowed = versionKey is not null && registry.IsSupported(versionKey);
|
bool allowed = versionKey is not null && registry.IsSupported(versionKey);
|
||||||
var status = allowed ? VersionStatus_ValidForPlay : VersionStatus_UpdateRequired;
|
var status = allowed ? VersionStatus_ValidForPlay : VersionStatus_UpdateRequired;
|
||||||
|
|||||||
@@ -668,11 +668,14 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
|||||||
[HttpPost("/members/bulk")]
|
[HttpPost("/members/bulk")]
|
||||||
public async Task<IActionResult> MembersBulk()
|
public async Task<IActionResult> MembersBulk()
|
||||||
{
|
{
|
||||||
|
var form = Request.HasFormContentType ? Request.Form : null;
|
||||||
if (!long.TryParse(Request.Query["clubId"].ToString(), out var clubId)
|
if (!long.TryParse(Request.Query["clubId"].ToString(), out var clubId)
|
||||||
&& !long.TryParse(Request.Form["clubId"].ToString(), out clubId))
|
&& (form is null || !long.TryParse(form["clubId"].ToString(), out clubId)))
|
||||||
return BadRequest(new { error = "missing_club_id" });
|
return BadRequest(new { error = "missing_club_id" });
|
||||||
|
|
||||||
var idsRaw = Request.Query["playerIds"].Concat(Request.Form["playerIds"]);
|
var idsRaw = form is null
|
||||||
|
? Request.Query["playerIds"].AsEnumerable()
|
||||||
|
: Request.Query["playerIds"].Concat(form["playerIds"]);
|
||||||
var ids = idsRaw
|
var ids = idsRaw
|
||||||
.SelectMany(v => (v ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
.SelectMany(v => (v ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
.Select(s => long.TryParse(s, out var n) ? n : 0)
|
.Select(s => long.TryParse(s, out var n) ? n : 0)
|
||||||
|
|||||||
@@ -223,9 +223,12 @@ public class MatchPlayerController(
|
|||||||
var playerId = TryGetCurrentPlayerId();
|
var playerId = TryGetCurrentPlayerId();
|
||||||
if (playerId == 0) return Ok();
|
if (playerId == 0) return Ok();
|
||||||
|
|
||||||
var form = Request.Form;
|
var regions = Request.HasFormContentType
|
||||||
var regions = form["region"].ToArray();
|
? Request.Form["region"].ToArray()
|
||||||
var pings = form["ping"].ToArray();
|
: Request.Query["region"].ToArray();
|
||||||
|
var pings = Request.HasFormContentType
|
||||||
|
? Request.Form["ping"].ToArray()
|
||||||
|
: Request.Query["ping"].ToArray();
|
||||||
var pairs = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
var pairs = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
for (var i = 0; i < Math.Min(regions.Length, pings.Length); i++)
|
for (var i = 0; i < Math.Min(regions.Length, pings.Length); i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ public class NsController : ControllerBase
|
|||||||
// code: locker)" in BootSequence.
|
// code: locker)" in BootSequence.
|
||||||
[HttpGet("/", Order = -1000)]
|
[HttpGet("/", Order = -1000)]
|
||||||
public ActionResult<Dictionary<string, string>> Root() =>
|
public ActionResult<Dictionary<string, string>> Root() =>
|
||||||
Ok(ConfigService.BuildServiceUrlMap(_domain.Apex));
|
Ok(ConfigService.BuildServiceUrlMap(_domain));
|
||||||
|
|
||||||
// Some builds fetch /v1 or /v1/services directly
|
// Some builds fetch /v1 or /v1/services directly
|
||||||
[HttpGet("/v1")]
|
[HttpGet("/v1")]
|
||||||
[HttpGet("/v1/services")]
|
[HttpGet("/v1/services")]
|
||||||
public ActionResult<Dictionary<string, string>> Services() =>
|
public ActionResult<Dictionary<string, string>> Services() =>
|
||||||
Ok(ConfigService.BuildServiceUrlMap(_domain.Apex));
|
Ok(ConfigService.BuildServiceUrlMap(_domain));
|
||||||
|
|
||||||
// Ping / health used by the client keep-alive loop
|
// Ping / health used by the client keep-alive loop
|
||||||
[HttpGet("/v1/ping")]
|
[HttpGet("/v1/ping")]
|
||||||
|
|||||||
@@ -180,8 +180,13 @@ public static class LegacyUpgrades
|
|||||||
// either no-op (Crescendo now exists) or the only writer.
|
// either no-op (Crescendo now exists) or the only writer.
|
||||||
for (var attempt = 1; attempt <= 2; attempt++)
|
for (var attempt = 1; attempt <= 2; attempt++)
|
||||||
{
|
{
|
||||||
var crescendoExists = await db.Rooms.AnyAsync(r => r.Name == "Crescendo", ct);
|
var renameCandidates = await db.Rooms
|
||||||
var bloodMoon = await db.Rooms.FirstOrDefaultAsync(r => r.Name == "BloodMoon", ct);
|
.Where(r => r.Name.ToLower() == "bloodmoon" || r.Name.ToLower() == "crescendo")
|
||||||
|
.ToListAsync(ct);
|
||||||
|
var crescendoExists = renameCandidates.Any(r =>
|
||||||
|
string.Equals(r.Name, "Crescendo", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var bloodMoon = renameCandidates.FirstOrDefault(r =>
|
||||||
|
string.Equals(r.Name, "BloodMoon", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (bloodMoon is null && crescendoExists)
|
if (bloodMoon is null && crescendoExists)
|
||||||
{
|
{
|
||||||
@@ -217,15 +222,15 @@ public static class LegacyUpgrades
|
|||||||
{
|
{
|
||||||
// Both exist — the rename ran on a different row at
|
// Both exist — the rename ran on a different row at
|
||||||
// some point, and the original BloodMoon row is now
|
// some point, and the original BloodMoon row is now
|
||||||
// a stale duplicate. Park it under CrescendoLegacy
|
// a stale duplicate. Park it under a unique legacy
|
||||||
// and hide from browse so /goto/name/... still
|
// name and hide from browse so /goto/name/... still
|
||||||
// resolves Crescendo to the right row.
|
// resolves Crescendo to the right row.
|
||||||
bloodMoon.Name = "CrescendoLegacy";
|
bloodMoon.Name = await PickUniqueRoomNameAsync(db, "CrescendoLegacy", bloodMoon.Id, ct);
|
||||||
bloodMoon.HiddenFromBrowse = true;
|
bloodMoon.HiddenFromBrowse = true;
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"[legacy-upgrade] RenameBloodMoonToCrescendo: applied — Rooms.Id={Id} parked as CrescendoLegacy (Crescendo already existed on a different row)",
|
"[legacy-upgrade] RenameBloodMoonToCrescendo: applied — Rooms.Id={Id} parked as {Name} (Crescendo already existed on a different row)",
|
||||||
bloodMoon.Id);
|
bloodMoon.Id, bloodMoon.Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,4 +246,22 @@ public static class LegacyUpgrades
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<string> PickUniqueRoomNameAsync(
|
||||||
|
DorkNetDbContext db, string preferred, long currentRoomId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var taken = await db.Rooms
|
||||||
|
.Where(r => r.Id != currentRoomId && r.Name.ToLower().StartsWith(preferred.ToLower()))
|
||||||
|
.Select(r => r.Name)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
if (!taken.Any(n => string.Equals(n, preferred, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return preferred;
|
||||||
|
|
||||||
|
var suffix = 2;
|
||||||
|
while (taken.Any(n => string.Equals(n, $"{preferred}{suffix}", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
suffix++;
|
||||||
|
|
||||||
|
return $"{preferred}{suffix}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
(generated by `dotnet ef migrations add Initial` against the
|
(generated by `dotnet ef migrations add Initial` against the
|
||||||
current entity model — captures all 67 `DbSet<>` tables).
|
current entity model — captures all 67 `DbSet<>` tables).
|
||||||
- **`LegacyUpgrades.cs`** holds idempotent data transforms that can't
|
- **`LegacyUpgrades.cs`** holds idempotent data transforms that can't
|
||||||
be derived from the entity model. Runs once per server boot, after
|
be derived from the entity model. Runs during host boot for the
|
||||||
EF migrations complete and before `RoomService.SeedAsync()`.
|
standalone server and for service-slice containers, after schema
|
||||||
|
bootstrap completes and before `RoomService.SeedAsync()`.
|
||||||
|
|
||||||
## What changed in the consolidation
|
## What changed in the consolidation
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ Both were collapsed in 2026-05-25:
|
|||||||
no per-feature patches needed.
|
no per-feature patches needed.
|
||||||
- The PhotonRegion coerce (the only data transform in the block)
|
- The PhotonRegion coerce (the only data transform in the block)
|
||||||
was moved into `LegacyUpgrades.CoercePhotonRegionAsync`.
|
was moved into `LegacyUpgrades.CoercePhotonRegionAsync`.
|
||||||
- The Storage admin page (`admin-ui/src/pages/Storage.tsx`) and
|
- The legacy storage backfill page (`admin-ui/src/pages/Storage.tsx`) and
|
||||||
`StorageBackfillController.cs` were deleted — they were the UI for
|
`StorageBackfillController.cs` were deleted — they were the UI for
|
||||||
draining the legacy `RoomDataBlobs.Bytes` column into S3, which is
|
draining the legacy `RoomDataBlobs.Bytes` column into S3, which is
|
||||||
no longer needed.
|
no longer needed.
|
||||||
@@ -37,8 +38,8 @@ Both were collapsed in 2026-05-25:
|
|||||||
## Production cutover (one-time SQL)
|
## Production cutover (one-time SQL)
|
||||||
|
|
||||||
Existing prod databases need a one-time fixup to tell EF "the new
|
Existing prod databases need a one-time fixup to tell EF "the new
|
||||||
Initial is already applied here" so the next boot doesn't try to
|
Initial is already applied here" so the next standalone or service-slice
|
||||||
`CREATE TABLE` over existing data.
|
boot doesn't try to `CREATE TABLE` over existing data.
|
||||||
|
|
||||||
### Postgres path
|
### Postgres path
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ table exists** because EF Core only creates it when `Migrate()` runs.
|
|||||||
Postgres continues to use `EnsureCreated()` post-consolidation, so
|
Postgres continues to use `EnsureCreated()` post-consolidation, so
|
||||||
there's *nothing to do* — `EnsureCreated()` reads the entity model,
|
there's *nothing to do* — `EnsureCreated()` reads the entity model,
|
||||||
sees the schema already matches, and no-ops. Just deploy the new
|
sees the schema already matches, and no-ops. Just deploy the new
|
||||||
build.
|
standalone image or microservices compose stack.
|
||||||
|
|
||||||
If you ever switch the Postgres path to `Migrate()`, run this once
|
If you ever switch the Postgres path to `Migrate()`, run this once
|
||||||
before the cutover deploy:
|
before the cutover deploy:
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
Generated: 2026-05-09. Source: client URLs from `Cpp2IL_ISIL` ISIL dump (curated list provided to this report); server routes harvested via grep of `[HttpGet]`/`[HttpPost]`/`[HttpPut]`/`[HttpDelete]`/`[Route]` attributes across `Controllers/**/*.cs` plus the namespace-wildcard fall-throughs in `ApiNamespaceStubsController` and the global `ApiCatchAllController` / `GlobalCatchAllController`.
|
Generated: 2026-05-09. Source: client URLs from `Cpp2IL_ISIL` ISIL dump (curated list provided to this report); server routes harvested via grep of `[HttpGet]`/`[HttpPost]`/`[HttpPut]`/`[HttpDelete]`/`[Route]` attributes across `Controllers/**/*.cs` plus the namespace-wildcard fall-throughs in `ApiNamespaceStubsController` and the global `ApiCatchAllController` / `GlobalCatchAllController`.
|
||||||
|
|
||||||
|
> Snapshot scope: this file is a historical controller-coverage matrix,
|
||||||
|
> not the live gateway route table. The dedicated microservice hosts
|
||||||
|
> still reuse the shared controller stack behind route guards, so the
|
||||||
|
> endpoint shape notes remain useful. For current service ownership, use
|
||||||
|
> `DorkNet.Contracts/DorkNetRouteOwnership.cs` or the gateway
|
||||||
|
> `/internal/routes` endpoint.
|
||||||
|
|
||||||
## How this matrix is built
|
## How this matrix is built
|
||||||
|
|
||||||
Each client URL is matched against the server's full route table. ASP.NET Core routes resolve "specific wins over wildcard", so a route literally registered as `api/foo/v1/bar` always wins over `api/foo/{*path}`. The `Status` column reflects the route that actually wins:
|
Each client URL is matched against the server's full route table. ASP.NET Core routes resolve "specific wins over wildcard", so a route literally registered as `api/foo/v1/bar` always wins over `api/foo/{*path}`. The `Status` column reflects the route that actually wins:
|
||||||
|
|||||||
@@ -73,6 +73,16 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DorkNet.Models\DorkNet.Models.csproj" />
|
<ProjectReference Include="..\DorkNet.Models\DorkNet.Models.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Service-slice containers reference DorkNet.Server for shared controllers
|
||||||
|
and services, but each slice owns its own appsettings.json. Without this
|
||||||
|
guard, dotnet publish sees both files targeting appsettings.json in the
|
||||||
|
same publish root and fails NETSDK1152. -->
|
||||||
|
<ItemGroup Condition="'$(DorkNetServiceContainerPublish)' == 'true'">
|
||||||
|
<Content Update="appsettings*.json" CopyToPublishDirectory="Never" />
|
||||||
|
<None Update="appsettings*.json" CopyToPublishDirectory="Never" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Vite admin SPA build. Source at admin-ui/, output goes to
|
<!-- Vite admin SPA build. Source at admin-ui/, output goes to
|
||||||
|
|||||||
@@ -15,3 +15,5 @@ var app = builder.Build();
|
|||||||
await app.RunDatabaseBootstrapAsync();
|
await app.RunDatabaseBootstrapAsync();
|
||||||
app.UseDorkNetPipeline();
|
app.UseDorkNetPipeline();
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
public partial class Program;
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ public class ConfigService(IConfiguration config, DomainConfig domain)
|
|||||||
return new RecRoomConfig
|
return new RecRoomConfig
|
||||||
{
|
{
|
||||||
MessageOfTheDay = config["Server:MOTD"] ?? "Welcome to the private server!",
|
MessageOfTheDay = config["Server:MOTD"] ?? "Welcome to the private server!",
|
||||||
CdnBaseUri = $"https://{domain.Sub("cdn")}",
|
CdnBaseUri = domain.Url("cdn"),
|
||||||
PhotonConfig = photon,
|
PhotonConfig = photon,
|
||||||
ServiceUrls = BuildServiceUrlMap(apex),
|
ServiceUrls = BuildServiceUrlMap(domain),
|
||||||
ConfigTable = new Dictionary<string, string>
|
ConfigTable = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
// Season keys the 2019/2020 client checks at startup
|
// Season keys the 2019/2020 client checks at startup
|
||||||
@@ -110,12 +110,14 @@ public class ConfigService(IConfiguration config, DomainConfig domain)
|
|||||||
/// <see cref="DomainConfig.Apex"/>) so the map stays a single
|
/// <see cref="DomainConfig.Apex"/>) so the map stays a single
|
||||||
/// source of truth.</summary>
|
/// source of truth.</summary>
|
||||||
public static Dictionary<string, string> BuildServiceUrlMap(string apex)
|
public static Dictionary<string, string> BuildServiceUrlMap(string apex)
|
||||||
|
=> BuildServiceUrlMap(new DomainConfig(apex));
|
||||||
|
|
||||||
|
public static Dictionary<string, string> BuildServiceUrlMap(DomainConfig domain)
|
||||||
{
|
{
|
||||||
var map = new Dictionary<string, string>(ServiceSubdomains.Length);
|
var map = new Dictionary<string, string>(ServiceSubdomains.Length);
|
||||||
foreach (var (service, sub) in ServiceSubdomains)
|
foreach (var (service, sub) in ServiceSubdomains)
|
||||||
{
|
{
|
||||||
var host = string.IsNullOrEmpty(sub) ? apex : $"{sub}.{apex}";
|
map[service] = domain.Url(sub);
|
||||||
map[service] = $"https://{host}";
|
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,28 @@ namespace DorkNet.Server.Services;
|
|||||||
public sealed class DomainConfig
|
public sealed class DomainConfig
|
||||||
{
|
{
|
||||||
public string Apex { get; }
|
public string Apex { get; }
|
||||||
public DomainConfig(string apex) { Apex = apex; }
|
public string Scheme { get; }
|
||||||
|
|
||||||
|
public DomainConfig(string apex, string scheme = "https")
|
||||||
|
{
|
||||||
|
Apex = apex;
|
||||||
|
Scheme = NormalizeScheme(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
public string Sub(string prefix) => $"{prefix}.{Apex}";
|
public string Sub(string prefix) => $"{prefix}.{Apex}";
|
||||||
|
public string Url(string prefix, string path = "")
|
||||||
|
{
|
||||||
|
var host = string.IsNullOrEmpty(prefix) ? Apex : Sub(prefix);
|
||||||
|
if (string.IsNullOrEmpty(path)) return $"{Scheme}://{host}";
|
||||||
|
return $"{Scheme}://{host}{(path.StartsWith('/') ? path : "/" + path)}";
|
||||||
|
}
|
||||||
|
|
||||||
public static bool MatchesSubdomain(string host, string prefix)
|
public static bool MatchesSubdomain(string host, string prefix)
|
||||||
=> host.StartsWith(prefix + ".", StringComparison.OrdinalIgnoreCase);
|
=> host.StartsWith(prefix + ".", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static string NormalizeScheme(string? value)
|
||||||
|
{
|
||||||
|
var scheme = string.IsNullOrWhiteSpace(value) ? "https" : value.Trim().TrimEnd(':', '/', '\\').ToLowerInvariant();
|
||||||
|
return scheme is "http" or "https" ? scheme : "https";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ public static class MiddlewarePipelineExtensions
|
|||||||
if (ctx.Request.Host.Host.Equals(domainCfg.Sub("api"), StringComparison.OrdinalIgnoreCase) &&
|
if (ctx.Request.Host.Host.Equals(domainCfg.Sub("api"), StringComparison.OrdinalIgnoreCase) &&
|
||||||
(ctx.Request.Path == "/admin" || ctx.Request.Path.StartsWithSegments("/admin")))
|
(ctx.Request.Path == "/admin" || ctx.Request.Path.StartsWithSegments("/admin")))
|
||||||
{
|
{
|
||||||
ctx.Response.Redirect($"https://{domainCfg.Sub("admin")}/", permanent: false);
|
ctx.Response.Redirect(domainCfg.Url("admin", "/"), permanent: false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using DorkNet.Contracts;
|
||||||
using DorkNet.Server.Compat;
|
using DorkNet.Server.Compat;
|
||||||
using DorkNet.Server.Data;
|
using DorkNet.Server.Data;
|
||||||
using DorkNet.Server.Services;
|
using DorkNet.Server.Services;
|
||||||
@@ -106,14 +107,19 @@ public static class ServiceCollectionExtensions
|
|||||||
builder.Configuration["Domain:Apex"]
|
builder.Configuration["Domain:Apex"]
|
||||||
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
|
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
|
||||||
?? "localhost";
|
?? "localhost";
|
||||||
builder.Services.AddSingleton(new DomainConfig(apex));
|
var scheme = builder.Configuration["Domain:Scheme"] ?? "https";
|
||||||
|
builder.Services.AddSingleton(new DomainConfig(apex, scheme));
|
||||||
|
var allowedHosts = new[] { apex, "localhost", "127.0.0.1" }
|
||||||
|
.Concat(DorkNetRouteOwnership.PublicSubdomains.Select(subdomain => $"{subdomain}.{apex}"))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
builder.Services.Configure<Microsoft.AspNetCore.HostFiltering.HostFilteringOptions>(opt =>
|
builder.Services.Configure<Microsoft.AspNetCore.HostFiltering.HostFilteringOptions>(opt =>
|
||||||
{
|
{
|
||||||
opt.AllowedHosts = new[] { apex, $"*.{apex}", "localhost", "127.0.0.1" };
|
opt.AllowedHosts = allowedHosts;
|
||||||
opt.AllowEmptyHosts = true;
|
opt.AllowEmptyHosts = true;
|
||||||
opt.IncludeFailureMessage = true;
|
opt.IncludeFailureMessage = true;
|
||||||
});
|
});
|
||||||
Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{apex}, *.{apex}, localhost]");
|
Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{string.Join(", ", allowedHosts)}]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Services ──────────────────────────────────────────────────────────────
|
// ── Services ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
|
<meta name="theme-color" content="#0b95c7" />
|
||||||
<title>DorkNet Admin</title>
|
<title>DorkNet Admin</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="%BASE_URL%brand/dorknet.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
import logoUrl from '../assets/dorknet.svg';
|
||||||
|
|
||||||
|
export function BrandMark({ className = 'size-8' }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex shrink-0 items-center justify-center rounded-md bg-ink-950 ring-1 ring-brand-300/30 shadow-lg shadow-brand-900/30 ${className}`}>
|
||||||
|
<img src={logoUrl} alt="" className="h-full w-full object-contain p-0.5" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
LayoutDashboard, Users, Building2, Activity,
|
LayoutDashboard, Users, Building2, Activity,
|
||||||
Megaphone, MessageSquare, Settings, X,
|
Megaphone, MessageSquare, Settings, X,
|
||||||
} from './Icons';
|
} from './Icons';
|
||||||
|
import { BrandMark } from './BrandMark';
|
||||||
|
|
||||||
// Consolidated navigation. Per-player moderation (bans, grants, gifts,
|
// Consolidated navigation. Per-player moderation (bans, grants, gifts,
|
||||||
// password resets) lives in the player detail modal; the old Bans /
|
// password resets) lives in the player detail modal; the old Bans /
|
||||||
@@ -67,9 +68,9 @@ export function Sidebar({ open, onClose, onNavigate }: { open: boolean; onClose:
|
|||||||
<>
|
<>
|
||||||
<div className="px-2 pb-4 flex items-center justify-between">
|
<div className="px-2 pb-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="size-8 rounded-md bg-gradient-to-br from-brand-400 to-brand-600 flex items-center justify-center font-bold text-white">D</div>
|
<BrandMark className="size-9" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-semibold tracking-tight text-ink-50">DorkNet</div>
|
<div className="text-sm font-semibold text-ink-50">DorkNet</div>
|
||||||
<div className="text-[10px] uppercase tracking-widest text-ink-400">admin</div>
|
<div className="text-[10px] uppercase tracking-widest text-ink-400">admin</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { api, ApiError } from '../lib/api';
|
import { api, ApiError } from '../lib/api';
|
||||||
import { notifyAuthChange, setSession } from '../lib/auth';
|
import { notifyAuthChange, setSession } from '../lib/auth';
|
||||||
|
import { BrandMark } from '../components/BrandMark';
|
||||||
|
|
||||||
interface LoginResponse {
|
interface LoginResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
@@ -51,12 +52,12 @@ export function Login() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-full flex items-center justify-center px-4 py-12 bg-gradient-to-br from-ink-950 via-ink-950 to-ink-900">
|
<div className="min-h-full flex items-center justify-center px-4 py-12 bg-[linear-gradient(135deg,#0a0b0e_0%,#0d1d25_48%,#131418_100%)]">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm">
|
||||||
<div className="mb-8 flex flex-col items-center gap-2">
|
<div className="mb-8 flex flex-col items-center gap-2">
|
||||||
<div className="size-12 rounded-xl bg-gradient-to-br from-brand-400 to-brand-600 flex items-center justify-center text-xl font-bold text-white shadow-lg shadow-brand-900/40">D</div>
|
<BrandMark className="size-16 rounded-xl" />
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-xl font-semibold tracking-tight text-ink-50">DorkNet Admin</h1>
|
<h1 className="text-xl font-semibold text-ink-50">DorkNet Admin</h1>
|
||||||
<p className="text-xs text-ink-400">Sign in with an admin account.</p>
|
<p className="text-xs text-ink-400">Sign in with an admin account.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -20,17 +20,17 @@ export default {
|
|||||||
950: '#0a0b0e',
|
950: '#0a0b0e',
|
||||||
},
|
},
|
||||||
brand: {
|
brand: {
|
||||||
DEFAULT: '#7c5cff',
|
DEFAULT: '#0b95c7',
|
||||||
50: '#f1edff',
|
50: '#ecfbff',
|
||||||
100: '#e1d8ff',
|
100: '#c8f3ff',
|
||||||
200: '#c5b6ff',
|
200: '#8bedff',
|
||||||
300: '#a48dff',
|
300: '#51e5ff',
|
||||||
400: '#8d72ff',
|
400: '#22cfee',
|
||||||
500: '#7c5cff',
|
500: '#0b95c7',
|
||||||
600: '#6948e3',
|
600: '#087aa8',
|
||||||
700: '#5236b8',
|
700: '#096083',
|
||||||
800: '#3e2989',
|
800: '#0a465f',
|
||||||
900: '#2a1c5f',
|
900: '#082f40',
|
||||||
},
|
},
|
||||||
success: '#3ecf8e',
|
success: '#3ecf8e',
|
||||||
warn: '#f4b740',
|
warn: '#f4b740',
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
|
<meta name="theme-color" content="#0b95c7" />
|
||||||
<title>DorkNet</title>
|
<title>DorkNet</title>
|
||||||
<meta name="description" content="A private Rec Room server. Browse the player photo feed, find friends, and explore community-made rooms." />
|
<meta name="description" content="A private Rec Room server. Browse the player photo feed, find friends, and explore community-made rooms." />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="%BASE_URL%brand/dorknet.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
import logoUrl from '../assets/dorknet.svg';
|
||||||
|
|
||||||
|
export function BrandMark({ className = 'size-8' }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex shrink-0 items-center justify-center rounded-md bg-ink-950 ring-1 ring-brand-300/30 shadow-lg shadow-brand-900/30 ${className}`}>
|
||||||
|
<img src={logoUrl} alt="" className="h-full w-full object-contain p-0.5" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NavLink, Link, useLocation, useNavigate } from 'react-router-dom';
|
import { NavLink, Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { BrandMark } from './BrandMark';
|
||||||
|
|
||||||
const NAV = [
|
const NAV = [
|
||||||
{ to: '/', label: 'Home', exact: true },
|
{ to: '/', label: 'Home', exact: true },
|
||||||
@@ -30,8 +31,8 @@ export function Header() {
|
|||||||
<header className="sticky top-0 z-40 border-b border-ink-800 bg-ink-950/85 backdrop-blur">
|
<header className="sticky top-0 z-40 border-b border-ink-800 bg-ink-950/85 backdrop-blur">
|
||||||
<div className="mx-auto max-w-6xl flex items-center gap-4 px-4 sm:px-6 py-3">
|
<div className="mx-auto max-w-6xl flex items-center gap-4 px-4 sm:px-6 py-3">
|
||||||
<Link to="/" className="flex items-center gap-2 shrink-0">
|
<Link to="/" className="flex items-center gap-2 shrink-0">
|
||||||
<span className="inline-block size-6 rounded-md bg-gradient-to-br from-brand-400 to-brand-700 shadow-inner" />
|
<BrandMark className="size-7" />
|
||||||
<span className="font-semibold text-ink-50 tracking-tight">DorkNet</span>
|
<span className="font-semibold text-ink-50">DorkNet</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="hidden md:flex items-center gap-1 ml-2">
|
<nav className="hidden md:flex items-center gap-1 ml-2">
|
||||||
|
|||||||
@@ -16,11 +16,9 @@ export function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
<section className="card !p-6 sm:!p-10 relative overflow-hidden">
|
<section className="card !p-6 sm:!p-10 relative overflow-hidden border-brand-500/20 bg-[linear-gradient(135deg,rgba(11,149,199,0.16),rgba(19,20,24,0.72)_42%,rgba(19,20,24,0.92))]">
|
||||||
<div className="absolute -top-20 -right-24 size-72 rounded-full bg-brand-500/20 blur-3xl pointer-events-none" />
|
|
||||||
<div className="absolute -bottom-20 -left-24 size-72 rounded-full bg-brand-700/20 blur-3xl pointer-events-none" />
|
|
||||||
<div className="relative max-w-2xl">
|
<div className="relative max-w-2xl">
|
||||||
<h1 className="text-3xl sm:text-5xl font-bold text-ink-50 tracking-tight">
|
<h1 className="text-3xl sm:text-5xl font-bold text-ink-50">
|
||||||
Rec Room, kept alive.
|
Rec Room, kept alive.
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-3 text-ink-300 text-base sm:text-lg">
|
<p className="mt-3 text-ink-300 text-base sm:text-lg">
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -20,17 +20,17 @@ export default {
|
|||||||
950: '#0a0b0e',
|
950: '#0a0b0e',
|
||||||
},
|
},
|
||||||
brand: {
|
brand: {
|
||||||
DEFAULT: '#7c5cff',
|
DEFAULT: '#0b95c7',
|
||||||
50: '#f1edff',
|
50: '#ecfbff',
|
||||||
100: '#e1d8ff',
|
100: '#c8f3ff',
|
||||||
200: '#c5b6ff',
|
200: '#8bedff',
|
||||||
300: '#a48dff',
|
300: '#51e5ff',
|
||||||
400: '#8d72ff',
|
400: '#22cfee',
|
||||||
500: '#7c5cff',
|
500: '#0b95c7',
|
||||||
600: '#6948e3',
|
600: '#087aa8',
|
||||||
700: '#5236b8',
|
700: '#096083',
|
||||||
800: '#3e2989',
|
800: '#0a465f',
|
||||||
900: '#2a1c5f',
|
900: '#082f40',
|
||||||
},
|
},
|
||||||
success: '#3ecf8e',
|
success: '#3ecf8e',
|
||||||
warn: '#f4b740',
|
warn: '#f4b740',
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -3,12 +3,17 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#0b95c7">
|
||||||
<title>DorkNet Admin</title>
|
<title>DorkNet Admin</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="brand/dorknet.svg">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="login" class="card">
|
<div id="login" class="card">
|
||||||
<h1>DorkNet Admin</h1>
|
<div class="brand-lockup">
|
||||||
|
<span class="logo-mark"><img src="brand/dorknet.svg" alt=""></span>
|
||||||
|
<h1>DorkNet Admin</h1>
|
||||||
|
</div>
|
||||||
<p class="muted">Sign in with an admin account.</p>
|
<p class="muted">Sign in with an admin account.</p>
|
||||||
<form id="loginForm">
|
<form id="loginForm">
|
||||||
<label>Username<input id="loginUsername" autocomplete="username" required></label>
|
<label>Username<input id="loginUsername" autocomplete="username" required></label>
|
||||||
@@ -20,7 +25,10 @@
|
|||||||
|
|
||||||
<div id="app" hidden>
|
<div id="app" hidden>
|
||||||
<header>
|
<header>
|
||||||
<h1>DorkNet Admin</h1>
|
<div class="brand-lockup">
|
||||||
|
<span class="logo-mark"><img src="brand/dorknet.svg" alt=""></span>
|
||||||
|
<h1>DorkNet Admin</h1>
|
||||||
|
</div>
|
||||||
<div class="who">
|
<div class="who">
|
||||||
Signed in as <span id="meName"></span>
|
Signed in as <span id="meName"></span>
|
||||||
<button id="logoutBtn" class="link">log out</button>
|
<button id="logoutBtn" class="link">log out</button>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
|
||||||
--bg: #0a0c10;
|
--bg: #0a0c10;
|
||||||
--bg-grad: radial-gradient(circle at 20% 0%, #16204a 0%, #0a0c10 60%);
|
--bg-grad: radial-gradient(circle at 18% -8%, rgba(11, 149, 199, 0.22) 0%, rgba(10, 12, 16, 0) 42%), linear-gradient(135deg, #0a0c10 0%, #0d1d25 48%, #0a0c10 100%);
|
||||||
--panel: rgba(22, 26, 35, 0.85);
|
--panel: rgba(22, 26, 35, 0.85);
|
||||||
--panel-solid: #161a23;
|
--panel-solid: #161a23;
|
||||||
--panel-2: #1d2230;
|
--panel-2: #1d2230;
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
--text-dim: #b3b9c9;
|
--text-dim: #b3b9c9;
|
||||||
--muted: #7a8195;
|
--muted: #7a8195;
|
||||||
|
|
||||||
--accent: #7aa9ff;
|
--accent: #33ebff;
|
||||||
--accent-2: #b48cff;
|
--accent-2: #0b95c7;
|
||||||
--accent-grad: linear-gradient(135deg, #7aa9ff 0%, #b48cff 100%);
|
--accent-grad: linear-gradient(135deg, #33ebff 0%, #0b95c7 100%);
|
||||||
--accent-hover: #93b9ff;
|
--accent-hover: #c8f3ff;
|
||||||
|
|
||||||
--good: #4ad28b;
|
--good: #4ad28b;
|
||||||
--good-bg: rgba(74, 210, 139, 0.12);
|
--good-bg: rgba(74, 210, 139, 0.12);
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
--warn-bg: rgba(255, 206, 91, 0.12);
|
--warn-bg: rgba(255, 206, 91, 0.12);
|
||||||
--danger: #ff6f7d;
|
--danger: #ff6f7d;
|
||||||
--danger-bg: rgba(255, 111, 125, 0.12);
|
--danger-bg: rgba(255, 111, 125, 0.12);
|
||||||
--info-bg: rgba(122, 169, 255, 0.12);
|
--info-bg: rgba(11, 149, 199, 0.12);
|
||||||
|
|
||||||
--radius: 10px;
|
--radius: 10px;
|
||||||
--radius-lg: 14px;
|
--radius-lg: 14px;
|
||||||
@@ -107,10 +107,10 @@ button[type="submit"], button.primary {
|
|||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
color: #0a0c10;
|
color: #0a0c10;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: 0 4px 14px rgba(122, 169, 255, 0.25);
|
box-shadow: 0 4px 14px rgba(11, 149, 199, 0.28);
|
||||||
}
|
}
|
||||||
button[type="submit"]:hover, button.primary:hover {
|
button[type="submit"]:hover, button.primary:hover {
|
||||||
box-shadow: 0 6px 20px rgba(122, 169, 255, 0.4);
|
box-shadow: 0 6px 20px rgba(51, 235, 255, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.danger {
|
button.danger {
|
||||||
@@ -158,7 +158,7 @@ input:hover, textarea:hover, select:hover { border-color: #3a425a; }
|
|||||||
input:focus, textarea:focus, select:focus {
|
input:focus, textarea:focus, select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 0 0 3px rgba(122, 169, 255, 0.15);
|
box-shadow: 0 0 0 3px rgba(11, 149, 199, 0.2);
|
||||||
}
|
}
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
@@ -215,6 +215,27 @@ label > select {
|
|||||||
#login p { margin: 0 0 22px; }
|
#login p { margin: 0 0 22px; }
|
||||||
#login button[type="submit"] { width: 100%; padding: 10px; font-size: 14px; }
|
#login button[type="submit"] { width: 100%; padding: 10px; font-size: 14px; }
|
||||||
|
|
||||||
|
.brand-lockup {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.logo-mark {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #0a0c10;
|
||||||
|
border: 1px solid rgba(51, 235, 255, 0.28);
|
||||||
|
box-shadow: 0 6px 18px rgba(11, 149, 199, 0.34);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.logo-mark img { width: 100%; height: 100%; object-fit: contain; padding: 1px; }
|
||||||
|
|
||||||
@keyframes fadeUp {
|
@keyframes fadeUp {
|
||||||
from { opacity: 0; transform: translateY(12px); }
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
@@ -280,7 +301,7 @@ nav .tab:hover { color: var(--text-dim); transform: none; background: var(--pane
|
|||||||
nav .tab.active {
|
nav .tab.active {
|
||||||
background: var(--info-bg);
|
background: var(--info-bg);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border-color: rgba(122, 169, 255, 0.25);
|
border-color: rgba(51, 235, 255, 0.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-panel {
|
.tab-panel {
|
||||||
@@ -382,7 +403,7 @@ nav .tab.active {
|
|||||||
}
|
}
|
||||||
.table tr:last-child td { border-bottom: none; }
|
.table tr:last-child td { border-bottom: none; }
|
||||||
.table tr { transition: background var(--t); }
|
.table tr { transition: background var(--t); }
|
||||||
.table tbody tr:hover td { background: rgba(122, 169, 255, 0.04); }
|
.table tbody tr:hover td { background: rgba(11, 149, 199, 0.06); }
|
||||||
|
|
||||||
.table .actions {
|
.table .actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -419,7 +440,7 @@ nav .tab.active {
|
|||||||
.badge.no-dot::before { display: none; }
|
.badge.no-dot::before { display: none; }
|
||||||
.badge.online { background: var(--good-bg); color: var(--good); border-color: rgba(74, 210, 139, 0.2); }
|
.badge.online { background: var(--good-bg); color: var(--good); border-color: rgba(74, 210, 139, 0.2); }
|
||||||
.badge.offline { background: rgba(139, 145, 158, 0.08); color: var(--muted); border-color: rgba(139, 145, 158, 0.18); }
|
.badge.offline { background: rgba(139, 145, 158, 0.08); color: var(--muted); border-color: rgba(139, 145, 158, 0.18); }
|
||||||
.badge.admin { background: var(--info-bg); color: var(--accent); border-color: rgba(122, 169, 255, 0.2); }
|
.badge.admin { background: var(--info-bg); color: var(--accent); border-color: rgba(51, 235, 255, 0.2); }
|
||||||
.badge.banned { background: var(--danger-bg); color: var(--danger); border-color: rgba(255, 111, 125, 0.2); }
|
.badge.banned { background: var(--danger-bg); color: var(--danger); border-color: rgba(255, 111, 125, 0.2); }
|
||||||
|
|
||||||
.badge-row { display: inline-flex; gap: 4px; flex-wrap: wrap; }
|
.badge-row { display: inline-flex; gap: 4px; flex-wrap: wrap; }
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -3,13 +3,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#0b95c7">
|
||||||
<title>DorkNet — Feed</title>
|
<title>DorkNet — Feed</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="brand/dorknet.svg">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<a href="/" class="logo">
|
<a href="/" class="logo">
|
||||||
<span class="logo-mark">●</span>
|
<span class="logo-mark"><img src="brand/dorknet.svg" alt=""></span>
|
||||||
<span class="logo-text">DorkNet</span>
|
<span class="logo-text">DorkNet</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--bg: #07080b;
|
--bg: #07080b;
|
||||||
--bg-grad: radial-gradient(circle at 20% -10%, #1d1748 0%, #07080b 55%);
|
--bg-grad: radial-gradient(circle at 18% -8%, rgba(11, 149, 199, 0.22) 0%, rgba(7, 8, 11, 0) 42%), linear-gradient(135deg, #07080b 0%, #0c171c 48%, #07080b 100%);
|
||||||
--panel: rgba(20, 22, 30, 0.7);
|
--panel: rgba(20, 22, 30, 0.7);
|
||||||
--panel-2: #161922;
|
--panel-2: #161922;
|
||||||
--panel-3: #1f2330;
|
--panel-3: #1f2330;
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
--text: #f0f2f8;
|
--text: #f0f2f8;
|
||||||
--text-dim: #b0b6c8;
|
--text-dim: #b0b6c8;
|
||||||
--muted: #757b8e;
|
--muted: #757b8e;
|
||||||
--accent: #ff6ec7;
|
--accent: #33ebff;
|
||||||
--accent-2: #7aa9ff;
|
--accent-2: #0b95c7;
|
||||||
--accent-grad: linear-gradient(135deg, #ff6ec7 0%, #b48cff 50%, #7aa9ff 100%);
|
--accent-grad: linear-gradient(135deg, #33ebff 0%, #0b95c7 100%);
|
||||||
--good: #4ad28b;
|
--good: #4ad28b;
|
||||||
--danger: #ff6f7d;
|
--danger: #ff6f7d;
|
||||||
--shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
--shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
||||||
@@ -41,7 +41,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a { color: var(--accent-2); text-decoration: none; transition: color var(--t); }
|
a { color: var(--accent-2); text-decoration: none; transition: color var(--t); }
|
||||||
a:hover { color: #a3c4ff; }
|
a:hover { color: #c8f3ff; }
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background: var(--panel-2);
|
background: var(--panel-2);
|
||||||
@@ -61,15 +61,15 @@ button.primary {
|
|||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
color: #07080b;
|
color: #07080b;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: 0 4px 18px rgba(255, 110, 199, 0.25);
|
box-shadow: 0 4px 18px rgba(11, 149, 199, 0.28);
|
||||||
}
|
}
|
||||||
button.primary:hover { box-shadow: 0 6px 24px rgba(255, 110, 199, 0.4); }
|
button.primary:hover { box-shadow: 0 6px 24px rgba(51, 235, 255, 0.28); }
|
||||||
button.ghost {
|
button.ghost {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
button.ghost:hover { color: var(--text); border-color: var(--accent); background: rgba(255,110,199,0.06); }
|
button.ghost:hover { color: var(--text); border-color: var(--accent); background: rgba(11, 149, 199, 0.1); }
|
||||||
|
|
||||||
/* ── Site shell ──────────────────────────────────────────────────── */
|
/* ── Site shell ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -97,13 +97,19 @@ button.ghost:hover { color: var(--text); border-color: var(--accent); background
|
|||||||
}
|
}
|
||||||
.logo:hover { color: var(--text); }
|
.logo:hover { color: var(--text); }
|
||||||
.logo-mark {
|
.logo-mark {
|
||||||
width: 22px;
|
width: 28px;
|
||||||
height: 22px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 8px;
|
||||||
background: var(--accent-grad);
|
background: #07080b;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
box-shadow: 0 4px 14px rgba(255, 110, 199, 0.5);
|
box-shadow: 0 4px 14px rgba(11, 149, 199, 0.42);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid rgba(51, 235, 255, 0.28);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.logo-mark img { width: 100%; height: 100%; object-fit: contain; padding: 1px; }
|
||||||
.site-nav {
|
.site-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace DorkNet.ServiceDefaults;
|
||||||
|
|
||||||
|
public static class DorkNetServiceDefaultsExtensions
|
||||||
|
{
|
||||||
|
public static WebApplicationBuilder AddDorkNetServiceDefaults(
|
||||||
|
this WebApplicationBuilder builder,
|
||||||
|
string serviceName)
|
||||||
|
{
|
||||||
|
builder.Services.AddSingleton(new DorkNetServiceInfo(serviceName));
|
||||||
|
builder.Services.AddHealthChecks();
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
builder.Services.ConfigureHttpJsonOptions(opt =>
|
||||||
|
opt.SerializerOptions.PropertyNamingPolicy = null);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebApplication MapDorkNetServiceDefaults(
|
||||||
|
this WebApplication app,
|
||||||
|
bool mapPublicHealth = true)
|
||||||
|
{
|
||||||
|
if (mapPublicHealth)
|
||||||
|
{
|
||||||
|
app.MapHealthChecks("/healthz");
|
||||||
|
}
|
||||||
|
|
||||||
|
app.MapGet("/internal/healthz", (DorkNetServiceInfo info) =>
|
||||||
|
{
|
||||||
|
return Results.Ok(new ServiceHealthResponse(
|
||||||
|
info.Name,
|
||||||
|
"ok",
|
||||||
|
DateTimeOffset.UtcNow));
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebApplication UseDorkNetRouteOwnershipGuard(
|
||||||
|
this WebApplication app,
|
||||||
|
string serviceName)
|
||||||
|
{
|
||||||
|
app.Use(async (ctx, next) =>
|
||||||
|
{
|
||||||
|
var domain = Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
|
||||||
|
?? ctx.RequestServices.GetService<Microsoft.Extensions.Configuration.IConfiguration>()?["Domain:Apex"]
|
||||||
|
?? "localhost";
|
||||||
|
var path = ctx.Request.Path.Value ?? "/";
|
||||||
|
if (DorkNetRouteOwnership.IsOwnedBy(serviceName, ctx.Request.Host.Host, path, domain))
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var owner = DorkNetRouteOwnership.ResolvePublicService(ctx.Request.Host.Host, path, domain);
|
||||||
|
var logger = ctx.RequestServices
|
||||||
|
.GetRequiredService<ILoggerFactory>()
|
||||||
|
.CreateLogger("DorkNet.ServiceDefaults.RouteGuard");
|
||||||
|
logger.LogDebug(
|
||||||
|
"[route-guard] service={Service} rejected {Method} {Host}{Path}; owner={Owner}",
|
||||||
|
serviceName,
|
||||||
|
ctx.Request.Method,
|
||||||
|
ctx.Request.Host,
|
||||||
|
ctx.Request.Path,
|
||||||
|
owner);
|
||||||
|
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||||
|
await ctx.Response.WriteAsJsonAsync(new
|
||||||
|
{
|
||||||
|
error = "route_not_owned_by_service",
|
||||||
|
service = serviceName,
|
||||||
|
owner,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace DorkNet.ServiceDefaults;
|
||||||
|
|
||||||
|
public sealed record DorkNetServiceInfo(string Name);
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Commerce);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/commerce/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Commerce,
|
||||||
|
[
|
||||||
|
"catalog",
|
||||||
|
"storefronts",
|
||||||
|
"econ",
|
||||||
|
"inventory",
|
||||||
|
"inventions",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/api/catalog/*",
|
||||||
|
"/api/storefronts/*",
|
||||||
|
"/econ/*",
|
||||||
|
"/api/equipment/*",
|
||||||
|
"/api/inventions/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Commerce);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Content);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/content/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Content,
|
||||||
|
[
|
||||||
|
"cdn",
|
||||||
|
"images",
|
||||||
|
"photos",
|
||||||
|
"room-blobs",
|
||||||
|
"storage",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/api/images/*",
|
||||||
|
"/api/photos/*",
|
||||||
|
"/data/*",
|
||||||
|
"/img/*",
|
||||||
|
"/upload",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Content);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Identity);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/identity/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Identity,
|
||||||
|
[
|
||||||
|
"accounts",
|
||||||
|
"auth",
|
||||||
|
"platform-login",
|
||||||
|
"jwt-issuance",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/account/*",
|
||||||
|
"/api/account/*",
|
||||||
|
"/api/platformlogin/*",
|
||||||
|
"/api/auth/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Identity);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Moderation);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/moderation/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Moderation,
|
||||||
|
[
|
||||||
|
"bug-reporting",
|
||||||
|
"player-reporting",
|
||||||
|
"sanitize",
|
||||||
|
"admin-api",
|
||||||
|
"testcase-management",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/api/bugreporting/*",
|
||||||
|
"/api/playerreporting/*",
|
||||||
|
"/api/sanitize/*",
|
||||||
|
"/api/admin/*",
|
||||||
|
"/api/testcasemanagement/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Moderation);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Notify);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/notify/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Notify,
|
||||||
|
[
|
||||||
|
"signalr-edge",
|
||||||
|
"notification-fanout",
|
||||||
|
"presence",
|
||||||
|
"player-request-logs",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/hub/v1",
|
||||||
|
"/api/notification/*",
|
||||||
|
"/player/*presence*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Notify);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Platform);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/platform/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Platform,
|
||||||
|
[
|
||||||
|
"service-directory",
|
||||||
|
"config",
|
||||||
|
"version-check",
|
||||||
|
"geo",
|
||||||
|
"strings",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/v1/services",
|
||||||
|
"/api/config/*",
|
||||||
|
"/api/versioncheck/*",
|
||||||
|
"/v1/regions",
|
||||||
|
"/strings/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Platform);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Rooms);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/rooms/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Rooms,
|
||||||
|
[
|
||||||
|
"rooms",
|
||||||
|
"subrooms",
|
||||||
|
"room-keys",
|
||||||
|
"matchmaking",
|
||||||
|
"discovery",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/rooms/*",
|
||||||
|
"/api/rooms/*",
|
||||||
|
"/roomserver/*",
|
||||||
|
"/v1/find",
|
||||||
|
"/v1/join/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Rooms);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Social);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/social/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Social,
|
||||||
|
[
|
||||||
|
"clubs",
|
||||||
|
"groups",
|
||||||
|
"announcements",
|
||||||
|
"player-events",
|
||||||
|
"subscriptions",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/club/*",
|
||||||
|
"/api/groups/*",
|
||||||
|
"/api/playerevents/*",
|
||||||
|
"/api/playersubscriptions/*",
|
||||||
|
"/announcements/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Social);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<DorkNetServerWebAsset Include="..\DorkNet.Server\wwwroot\**\*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="CopyDorkNetServerWebAssets"
|
||||||
|
AfterTargets="Publish"
|
||||||
|
Condition="'$(DorkNetServiceContainerPublish)' == 'true'">
|
||||||
|
<Copy SourceFiles="@(DorkNetServerWebAsset)"
|
||||||
|
DestinationFiles="@(DorkNetServerWebAsset->'$(PublishDir)wwwroot/%(RecursiveDir)%(Filename)%(Extension)')"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Web);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/web/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Web,
|
||||||
|
[
|
||||||
|
"public-site",
|
||||||
|
"admin-site",
|
||||||
|
"feed-site",
|
||||||
|
"site-api",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/",
|
||||||
|
"www.*",
|
||||||
|
"admin.*",
|
||||||
|
"feed.*",
|
||||||
|
"/api/site/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Web);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,13 +7,37 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Server", "DorkNet.S
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Models", "DorkNet.Models\DorkNet.Models.csproj", "{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Models", "DorkNet.Models\DorkNet.Models.csproj", "{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{07C2787E-EAC7-C090-1BA3-A61EC2A24D84}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MigrateSqliteToPostgres", "Tools\MigrateSqliteToPostgres\MigrateSqliteToPostgres.csproj", "{70C60098-1B11-4C13-943B-A8A056429EAE}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.AdminMobile", "DorkNet.AdminMobile\DorkNet.AdminMobile.csproj", "{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.AdminMobile", "DorkNet.AdminMobile\DorkNet.AdminMobile.csproj", "{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.ApiRouteCoverage", "tools\DorkNet.ApiRouteCoverage\DorkNet.ApiRouteCoverage.csproj", "{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Contracts", "DorkNet.Contracts\DorkNet.Contracts.csproj", "{7D9E995E-5333-43B2-848B-3522C6C3D893}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.ServiceDefaults", "DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj", "{0A66087B-37DF-401D-AFAC-01E7324C2460}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Gateway", "DorkNet.Gateway\DorkNet.Gateway.csproj", "{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Identity", "DorkNet.Services.Identity\DorkNet.Services.Identity.csproj", "{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Rooms", "DorkNet.Services.Rooms\DorkNet.Services.Rooms.csproj", "{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Notify", "DorkNet.Services.Notify\DorkNet.Services.Notify.csproj", "{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Server.Tests", "DorkNet.Server.Tests\DorkNet.Server.Tests.csproj", "{43230177-A02F-440A-9B59-B79AB5E7BC27}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Content", "DorkNet.Services.Content\DorkNet.Services.Content.csproj", "{8958DA76-3925-4F12-8884-DE2E79FE9677}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Social", "DorkNet.Services.Social\DorkNet.Services.Social.csproj", "{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Commerce", "DorkNet.Services.Commerce\DorkNet.Services.Commerce.csproj", "{77BBD5A3-A610-4A20-A61A-B4AB56291C27}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Platform", "DorkNet.Services.Platform\DorkNet.Services.Platform.csproj", "{C68AA2CA-9E80-46E5-9EE0-1544CE070036}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Moderation", "DorkNet.Services.Moderation\DorkNet.Services.Moderation.csproj", "{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Services.Web", "DorkNet.Services.Web\DorkNet.Services.Web.csproj", "{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{07C2787E-EAC7-C090-1BA3-A61EC2A24D84}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.ApiRouteCoverage", "tools\DorkNet.ApiRouteCoverage\DorkNet.ApiRouteCoverage.csproj", "{6BC447E7-3158-4FEC-81D4-506452157810}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -49,18 +73,6 @@ Global
|
|||||||
{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x64.Build.0 = Release|Any CPU
|
{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x86.ActiveCfg = Release|Any CPU
|
{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x86.Build.0 = Release|Any CPU
|
{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -73,24 +85,179 @@ Global
|
|||||||
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Release|x64.Build.0 = Release|Any CPU
|
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Release|x86.ActiveCfg = Release|Any CPU
|
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Release|x86.Build.0 = Release|Any CPU
|
{C2265F00-DFC5-4352-A94C-B887E3C0E5CA}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Debug|x64.Build.0 = Debug|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Debug|x86.Build.0 = Debug|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|x64.ActiveCfg = Release|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|x64.Build.0 = Release|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|x86.ActiveCfg = Release|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|x86.Build.0 = Release|Any CPU
|
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{0A66087B-37DF-401D-AFAC-01E7324C2460}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{1A7D03D5-DAC5-474C-9A21-8F2EEF82396F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{E85B21BA-ED08-4CED-ADB5-6B5E5ADEEBD1}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{3F01BA71-BD1E-461B-BCD0-F2E9CC92B640}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{5A6D7277-0974-4F12-AD9D-C705F5CFE19C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{43230177-A02F-440A-9B59-B79AB5E7BC27}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{8958DA76-3925-4F12-8884-DE2E79FE9677}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{C6DBC037-1E6B-4B2F-8490-08A6F9380E00}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{77BBD5A3-A610-4A20-A61A-B4AB56291C27}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{C68AA2CA-9E80-46E5-9EE0-1544CE070036}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{1D44C401-17B8-4314-9425-AE7A2CEDBBDE}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{F29F1B14-B61E-47CC-820A-C98BAE8F21DB}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{6BC447E7-3158-4FEC-81D4-506452157810}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{70C60098-1B11-4C13-943B-A8A056429EAE} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
|
{6BC447E7-3158-4FEC-81D4-506452157810} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
|
||||||
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ main also auto-detects your install.
|
|||||||
| **Version key** (sent as `X-DorkNet-Version` header) | `december_2020_12_18` |
|
| **Version key** (sent as `X-DorkNet-Version` header) | `december_2020_12_18` |
|
||||||
| **Version plugin** | `DorkNet.Server/Versions/Late2020/Late2020VersionPlugin.cs` |
|
| **Version plugin** | `DorkNet.Server/Versions/Late2020/Late2020VersionPlugin.cs` |
|
||||||
| **Schema** | One EF `Initial` migration; SQLite + Postgres both work |
|
| **Schema** | One EF `Initial` migration; SQLite + Postgres both work |
|
||||||
|
| **Runtime topology** | Gateway-fronted microservices with dedicated service slices and a monolith fallback |
|
||||||
| **Diverges from `march-2020-03-10` by** | ~170 files |
|
| **Diverges from `march-2020-03-10` by** | ~170 files |
|
||||||
|
|
||||||
Subsystems present on this branch that the March branch doesn't have:
|
Subsystems present on this branch that the March branch doesn't have:
|
||||||
@@ -58,7 +59,7 @@ cd DorkNet
|
|||||||
|
|
||||||
| Config key | Env var | Required | Notes |
|
| Config key | Env var | Required | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `Jwt:Secret` | `Jwt__Secret` | yes | ≥64 random chars |
|
| `Jwt:Secret` | `DORKNET_JWT_SECRET` or `Jwt__Secret` | yes | >=64 random chars |
|
||||||
| `Photon:AppId` | `Photon__AppId` | yes | Photon Realtime AppId (free at dashboard.photonengine.com) |
|
| `Photon:AppId` | `Photon__AppId` | yes | Photon Realtime AppId (free at dashboard.photonengine.com) |
|
||||||
| `Photon:VoiceAppId` | `Photon__VoiceAppId` | no | only if voice chat |
|
| `Photon:VoiceAppId` | `Photon__VoiceAppId` | no | only if voice chat |
|
||||||
| `Photon:CloudRegion` | `Photon__CloudRegion` | no | defaults to `us` |
|
| `Photon:CloudRegion` | `Photon__CloudRegion` | no | defaults to `us` |
|
||||||
@@ -66,8 +67,12 @@ cd DorkNet
|
|||||||
| `ConnectionStrings:Default` | `ConnectionStrings__Default` | postgres-only | full Npgsql connection string |
|
| `ConnectionStrings:Default` | `ConnectionStrings__Default` | postgres-only | full Npgsql connection string |
|
||||||
| `ConnectionStrings:Redis` | `ConnectionStrings__Redis` | no | enables SignalR backplane |
|
| `ConnectionStrings:Redis` | `ConnectionStrings__Redis` | no | enables SignalR backplane |
|
||||||
| `Domain:Apex` | `DORKNET_DOMAIN` | no | defaults to `localhost`; set for production |
|
| `Domain:Apex` | `DORKNET_DOMAIN` | no | defaults to `localhost`; set for production |
|
||||||
|
| `S3:Endpoint` | `S3__Endpoint` | production | S3-compatible API endpoint for blobs/images |
|
||||||
|
| `S3:AccessKey` | `S3__AccessKey` | production | S3 access key |
|
||||||
|
| `S3:SecretKey` | `S3__SecretKey` | production | S3 secret key |
|
||||||
|
| `S3:Region` | `S3__Region` | no | defaults to `garage`; use `auto` for R2 |
|
||||||
|
|
||||||
**4. Boot:**
|
**4. Boot a local standalone server:**
|
||||||
|
|
||||||
```pwsh
|
```pwsh
|
||||||
dotnet run --project DorkNet.Server
|
dotnet run --project DorkNet.Server
|
||||||
@@ -79,8 +84,9 @@ categories / playlists, and starts listening. You should see
|
|||||||
`Application started.` in the log; `curl http://localhost:8080/healthz`
|
`Application started.` in the log; `curl http://localhost:8080/healthz`
|
||||||
returns 200.
|
returns 200.
|
||||||
|
|
||||||
For a full Docker / VPS deploy, see
|
For the production microservices stack, Docker, and Dokploy deploy
|
||||||
[main's Advanced setup guide](../../blob/main/docs/advanced-setup.md).
|
notes, see
|
||||||
|
[docs/deploy.md](docs/deploy.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -194,15 +200,54 @@ Default `0` (off).
|
|||||||
|
|
||||||
| Folder | Purpose |
|
| Folder | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `DorkNet.Server/` | ASP.NET Core backend (controllers, services, data, hubs, middleware) |
|
| `DorkNet.Server/` | Shared ASP.NET Core controller/service stack, standalone fallback, monolith fallback |
|
||||||
| `DorkNet.Models/` | DTOs shared between server and client mod |
|
| `DorkNet.Models/` | DTOs shared between server and client mod |
|
||||||
|
| `DorkNet.Contracts/` | Shared service names, route ownership, service-map options, and health/probe contracts |
|
||||||
|
| `DorkNet.ServiceDefaults/` | Shared service health, JSON, HTTP client, service identity, and route-guard setup |
|
||||||
|
| `DorkNet.Gateway/` | Edge reverse proxy with service-map, route-table, and service-health endpoints |
|
||||||
|
| `DorkNet.Services.*` | Gateway-fronted service hosts for identity, rooms, notify, content, social, commerce, platform, moderation, and web |
|
||||||
| `DorkNet.ClientMod/` | MelonLoader IL2CPP mod — the client patcher |
|
| `DorkNet.ClientMod/` | MelonLoader IL2CPP mod — the client patcher |
|
||||||
| `tools/` | Installers (`install-melon.ps1`, `install-legacy-client.ps1`, `remove-eac.ps1`), Cloudflare tunnel templates |
|
| `tools/` | Installers (`install-melon.ps1`, `install-legacy-client.ps1`, `remove-eac.ps1`), Cloudflare tunnel templates |
|
||||||
|
| `Dockerfile`, `Dockerfile.service`, `docker-compose.microservices*.yml` | Standalone fallback image, reusable service image, and local/Dokploy microservices entrypoints |
|
||||||
|
|
||||||
See [docs/architecture.md](docs/architecture.md) for the full mental
|
See [docs/architecture.md](docs/architecture.md) for the full mental
|
||||||
model (project layout, request lifecycle, watch-mirror controller
|
model (project layout, request lifecycle, watch-mirror controller
|
||||||
pattern, where to start contributing).
|
pattern, where to start contributing).
|
||||||
|
|
||||||
|
### Microservices
|
||||||
|
|
||||||
|
The production topology is the gateway-fronted compose stack.
|
||||||
|
`docker-compose.microservices.yml` starts the gateway, identity, rooms,
|
||||||
|
notify, content, social, commerce, platform, moderation, web, and
|
||||||
|
monolith fallback service hosts plus Compose-managed Postgres and Redis
|
||||||
|
for local testing. `docker-compose.microservices.dokploy.yml` is the
|
||||||
|
Dokploy version; it adds a `cloudflared` sidecar so Cloudflare Tunnel
|
||||||
|
can route the apex and wildcard hostnames to the gateway without Dokploy
|
||||||
|
domain rows.
|
||||||
|
|
||||||
|
The gateway routes owned host/path slices to the dedicated service hosts.
|
||||||
|
The `web` service owns the apex, `www`, `admin`, and `feed` static hosts.
|
||||||
|
The monolith fallback is still present for unknown route families, so
|
||||||
|
public client URLs remain stable while the split continues.
|
||||||
|
|
||||||
|
The compose file intentionally does not start object storage. Point it at
|
||||||
|
your separate S3-compatible instance:
|
||||||
|
|
||||||
|
```pwsh
|
||||||
|
$env:DORKNET_S3_ENDPOINT="https://your-s3-endpoint"
|
||||||
|
$env:DORKNET_S3_ACCESS_KEY="..."
|
||||||
|
$env:DORKNET_S3_SECRET_KEY="..."
|
||||||
|
$env:DORKNET_S3_REGION="garage" # or auto for R2
|
||||||
|
docker compose -f docker-compose.microservices.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
For deployment details, including Dokploy env vars, Cloudflare Tunnel
|
||||||
|
routing, external Postgres, and S3 setup, see
|
||||||
|
[docs/deploy.md](docs/deploy.md).
|
||||||
|
|
||||||
|
For the browser admin console, its routes, auth model, and production
|
||||||
|
troubleshooting, see [docs/admin.md](docs/admin.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Database & migrations
|
## Database & migrations
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
x-dorknet-service-env: &dorknet-service-env
|
||||||
|
DORKNET_DOMAIN: ${DORKNET_DOMAIN:?Set DORKNET_DOMAIN in Dokploy}
|
||||||
|
DORKNET_JWT_SECRET: ${DORKNET_JWT_SECRET:?Set DORKNET_JWT_SECRET in Dokploy}
|
||||||
|
Database__Provider: postgres
|
||||||
|
ConnectionStrings__Default: ${DORKNET_POSTGRES_CONNECTION_STRING:-Host=postgres;Port=5432;Database=dorknet;Username=dorknet;Password=${POSTGRES_PASSWORD:-dorknet_dev}}
|
||||||
|
ConnectionStrings__Redis: redis:6379
|
||||||
|
S3__Endpoint: ${DORKNET_S3_ENDPOINT:?Set DORKNET_S3_ENDPOINT in Dokploy}
|
||||||
|
S3__AccessKey: ${DORKNET_S3_ACCESS_KEY:?Set DORKNET_S3_ACCESS_KEY in Dokploy}
|
||||||
|
S3__SecretKey: ${DORKNET_S3_SECRET_KEY:?Set DORKNET_S3_SECRET_KEY in Dokploy}
|
||||||
|
S3__Region: ${DORKNET_S3_REGION:-garage}
|
||||||
|
Photon__AppId: ${Photon__AppId:?Set Photon__AppId in Dokploy}
|
||||||
|
Photon__VoiceAppId: ${Photon__VoiceAppId:-}
|
||||||
|
Photon__CloudRegion: ${Photon__CloudRegion:-eu}
|
||||||
|
DorkNet__DefaultClientVersion: ${DORKNET_DEFAULT_CLIENT_VERSION:-december_2020_12_18}
|
||||||
|
DorkNet__SupportedVersions__0: ${DORKNET_SUPPORTED_VERSION:-december_2020_12_18}
|
||||||
|
DorkNet__BuildIdToVersionKey__20201210: ${DORKNET_DECEMBER_BUILD_VERSION_KEY:-december_2020_12_18}
|
||||||
|
|
||||||
|
x-dorknet-app-networks: &dorknet-app-networks
|
||||||
|
networks:
|
||||||
|
# Internal only — backends are reached by the gateway over `default`,
|
||||||
|
# never directly by Traefik, so keep them off dokploy-network.
|
||||||
|
- default
|
||||||
|
|
||||||
|
services:
|
||||||
|
gateway:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Gateway/DorkNet.Gateway.csproj
|
||||||
|
APP_DLL: DorkNet.Gateway.dll
|
||||||
|
environment:
|
||||||
|
DORKNET_DOMAIN: ${DORKNET_DOMAIN:?Set DORKNET_DOMAIN in Dokploy}
|
||||||
|
DorkNet__Services__Identity: http://identity:8080
|
||||||
|
DorkNet__Services__Rooms: http://rooms:8080
|
||||||
|
DorkNet__Services__Notify: http://notify:8080
|
||||||
|
DorkNet__Services__Content: http://content:8080
|
||||||
|
DorkNet__Services__Social: http://social:8080
|
||||||
|
DorkNet__Services__Commerce: http://commerce:8080
|
||||||
|
DorkNet__Services__Platform: http://platform:8080
|
||||||
|
DorkNet__Services__Moderation: http://moderation:8080
|
||||||
|
DorkNet__Services__Web: http://web:8080
|
||||||
|
DorkNet__Services__Monolith: http://monolith:8080
|
||||||
|
expose:
|
||||||
|
- "8080"
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- dokploy-network
|
||||||
|
labels:
|
||||||
|
# Cloudflare terminates TLS at the edge; the tunnel -> Traefik leg is
|
||||||
|
# internal HTTP, so route on the `web` entrypoint with no certresolver.
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.docker.network=dokploy-network
|
||||||
|
# Scope routing to YOUR domain (apex + subdomains) instead of catch-all:
|
||||||
|
- traefik.http.routers.dorknet-gw.rule=Host(`${DORKNET_DOMAIN}`) || HostRegexp(`^.+\.${DORKNET_DOMAIN}$`)
|
||||||
|
- traefik.http.routers.dorknet-gw.entrypoints=web
|
||||||
|
- traefik.http.services.dorknet-gw.loadbalancer.server.port=8080
|
||||||
|
depends_on:
|
||||||
|
- identity
|
||||||
|
- rooms
|
||||||
|
- notify
|
||||||
|
- content
|
||||||
|
- social
|
||||||
|
- commerce
|
||||||
|
- platform
|
||||||
|
- moderation
|
||||||
|
- web
|
||||||
|
- monolith
|
||||||
|
|
||||||
|
cloudflared:
|
||||||
|
image: cloudflare/cloudflared:latest
|
||||||
|
command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN:?Set CLOUDFLARE_TUNNEL_TOKEN in Dokploy}
|
||||||
|
# Must share dokploy-network so the tunnel can reach the Traefik
|
||||||
|
# container (dokploy-traefik) by name and hand traffic off to it.
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
depends_on:
|
||||||
|
- gateway
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
identity:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Identity/DorkNet.Services.Identity.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Identity.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
<<: *dorknet-app-networks
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
rooms:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Rooms/DorkNet.Services.Rooms.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Rooms.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
<<: *dorknet-app-networks
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
notify:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Notify/DorkNet.Services.Notify.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Notify.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
<<: *dorknet-app-networks
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
content:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Content/DorkNet.Services.Content.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Content.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
<<: *dorknet-app-networks
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
social:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Social/DorkNet.Services.Social.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Social.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
<<: *dorknet-app-networks
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
commerce:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Commerce/DorkNet.Services.Commerce.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Commerce.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
<<: *dorknet-app-networks
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
platform:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Platform/DorkNet.Services.Platform.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Platform.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
<<: *dorknet-app-networks
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
moderation:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Moderation/DorkNet.Services.Moderation.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Moderation.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
<<: *dorknet-app-networks
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Web/DorkNet.Services.Web.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Web.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
<<: *dorknet-app-networks
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
monolith:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
<<: *dorknet-app-networks
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: dorknet
|
||||||
|
POSTGRES_USER: dorknet
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dorknet_dev}
|
||||||
|
volumes:
|
||||||
|
- microservices-postgres:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
microservices-postgres:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dokploy-network:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
x-dorknet-service-env: &dorknet-service-env
|
||||||
|
DORKNET_DOMAIN: ${DORKNET_DOMAIN:-localhost}
|
||||||
|
DORKNET_JWT_SECRET: ${DORKNET_JWT_SECRET:-dorknet-dev-jwt-secret-000000000000000000000000000000000000000000}
|
||||||
|
Database__Provider: postgres
|
||||||
|
ConnectionStrings__Default: ${DORKNET_POSTGRES_CONNECTION_STRING:-Host=postgres;Port=5432;Database=dorknet;Username=dorknet;Password=${POSTGRES_PASSWORD:-dorknet_dev}}
|
||||||
|
ConnectionStrings__Redis: redis:6379
|
||||||
|
S3__Endpoint: ${DORKNET_S3_ENDPOINT:-}
|
||||||
|
S3__AccessKey: ${DORKNET_S3_ACCESS_KEY:-}
|
||||||
|
S3__SecretKey: ${DORKNET_S3_SECRET_KEY:-}
|
||||||
|
S3__Region: ${DORKNET_S3_REGION:-garage}
|
||||||
|
Photon__AppId: ${Photon__AppId:-}
|
||||||
|
Photon__VoiceAppId: ${Photon__VoiceAppId:-}
|
||||||
|
Photon__CloudRegion: ${Photon__CloudRegion:-eu}
|
||||||
|
DorkNet__DefaultClientVersion: ${DORKNET_DEFAULT_CLIENT_VERSION:-december_2020_12_18}
|
||||||
|
DorkNet__SupportedVersions__0: ${DORKNET_SUPPORTED_VERSION:-december_2020_12_18}
|
||||||
|
DorkNet__BuildIdToVersionKey__20201210: ${DORKNET_DECEMBER_BUILD_VERSION_KEY:-december_2020_12_18}
|
||||||
|
|
||||||
|
services:
|
||||||
|
gateway:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Gateway/DorkNet.Gateway.csproj
|
||||||
|
APP_DLL: DorkNet.Gateway.dll
|
||||||
|
environment:
|
||||||
|
DORKNET_DOMAIN: ${DORKNET_DOMAIN:-localhost}
|
||||||
|
DorkNet__Services__Identity: http://identity:8080
|
||||||
|
DorkNet__Services__Rooms: http://rooms:8080
|
||||||
|
DorkNet__Services__Notify: http://notify:8080
|
||||||
|
DorkNet__Services__Content: http://content:8080
|
||||||
|
DorkNet__Services__Social: http://social:8080
|
||||||
|
DorkNet__Services__Commerce: http://commerce:8080
|
||||||
|
DorkNet__Services__Platform: http://platform:8080
|
||||||
|
DorkNet__Services__Moderation: http://moderation:8080
|
||||||
|
DorkNet__Services__Web: http://web:8080
|
||||||
|
DorkNet__Services__Monolith: http://monolith:8080
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
depends_on:
|
||||||
|
- identity
|
||||||
|
- rooms
|
||||||
|
- notify
|
||||||
|
- content
|
||||||
|
- social
|
||||||
|
- commerce
|
||||||
|
- platform
|
||||||
|
- moderation
|
||||||
|
- web
|
||||||
|
- monolith
|
||||||
|
|
||||||
|
identity:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Identity/DorkNet.Services.Identity.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Identity.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
rooms:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Rooms/DorkNet.Services.Rooms.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Rooms.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
notify:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Notify/DorkNet.Services.Notify.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Notify.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
content:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Content/DorkNet.Services.Content.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Content.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
social:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Social/DorkNet.Services.Social.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Social.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
commerce:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Commerce/DorkNet.Services.Commerce.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Commerce.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
platform:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Platform/DorkNet.Services.Platform.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Platform.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
moderation:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Moderation/DorkNet.Services.Moderation.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Moderation.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.service
|
||||||
|
args:
|
||||||
|
PROJECT_PATH: DorkNet.Services.Web/DorkNet.Services.Web.csproj
|
||||||
|
APP_DLL: DorkNet.Services.Web.dll
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
monolith:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
environment:
|
||||||
|
<<: *dorknet-service-env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- redis
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: dorknet
|
||||||
|
POSTGRES_USER: dorknet
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dorknet_dev}
|
||||||
|
volumes:
|
||||||
|
- microservices-postgres:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
microservices-postgres:
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# DorkNet Admin Page
|
||||||
|
|
||||||
|
The admin page is the browser operations console for DorkNet. In the
|
||||||
|
microservices deployment it lives at `https://admin.<domain>/` and is
|
||||||
|
served by the `web` service from the built Vite SPA assets.
|
||||||
|
|
||||||
|
The admin backend endpoints are under `/api/admin/v1`. Requests from the
|
||||||
|
SPA are same-origin, so `https://admin.<domain>/api/admin/v1/*` is also
|
||||||
|
handled by the `web` service route on the admin host. The same
|
||||||
|
`/api/admin/*` route family is available as a path-owned moderation slice
|
||||||
|
for non-admin-host routing, so `/internal/routes` can show moderation as
|
||||||
|
the owner for that path family.
|
||||||
|
|
||||||
|
## Runtime Routing
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|---|---|
|
||||||
|
| SPA source | `DorkNet.Server/admin-ui` |
|
||||||
|
| Build output | `DorkNet.Server/wwwroot/admin` |
|
||||||
|
| Container path | `/app/wwwroot/admin` |
|
||||||
|
| Public browser host | `admin.<domain>` |
|
||||||
|
| API base used by the SPA | `/api/admin/v1` |
|
||||||
|
| Login endpoint | `POST /api/admin/v1/login` |
|
||||||
|
| Same-origin admin-host service | `web` |
|
||||||
|
| Path-routed admin API slice | `moderation` |
|
||||||
|
|
||||||
|
`DorkNet.Gateway` preserves the original Host header when it proxies to
|
||||||
|
backend services. That matters for the admin page: the `admin.<domain>`
|
||||||
|
host route must reach `web`, and `web` must contain
|
||||||
|
`/app/wwwroot/admin/index.html`.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
The first account created on a fresh database is promoted to admin so
|
||||||
|
there is always a bootstrap operator. The login form posts to
|
||||||
|
`/api/admin/v1/login`; the SPA stores the returned values in browser
|
||||||
|
`localStorage`:
|
||||||
|
|
||||||
|
| Key | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `dorknet.admin.token` | DorkNet admin JWT |
|
||||||
|
| `dorknet.admin.me` | Admin identity object used by the layout |
|
||||||
|
|
||||||
|
Every admin API request sends `Authorization: Bearer <token>`. Protected
|
||||||
|
actions are guarded by `AdminOnlyAttribute`, which checks the resolved
|
||||||
|
player row still has `IsAdmin = true`. A `401` clears the local session
|
||||||
|
and sends the browser back to login.
|
||||||
|
|
||||||
|
If the admin host is public, put it behind Cloudflare Access or an
|
||||||
|
equivalent outer access control. The DorkNet JWT is still required after
|
||||||
|
that outer check.
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
| Section | Routes | Workflows |
|
||||||
|
|---|---|---|
|
||||||
|
| Overview | `/` | Live ops dashboard, online players, active sessions, quick kick/ban/broadcast actions |
|
||||||
|
| Moderation | `/players` | Player directory, bans, reports, per-player ban/grant/gift/password/avatar/account actions |
|
||||||
|
| Activity | `/activity` | Admin audit log and per-player request logs |
|
||||||
|
| Content | `/rooms`, `/rooms/:id`, `/import-room`, `/content` | Room list/detail, room import, instances, leaderboards, community board, loading tips |
|
||||||
|
| Operations | `/broadcast`, `/settings` | Server broadcast, server toggles, signup codes, weekly challenges, Play menu tags, Rec Center doors, game config values |
|
||||||
|
|
||||||
|
Several older admin URLs are kept as redirects:
|
||||||
|
|
||||||
|
| Old route | Current route |
|
||||||
|
|---|---|
|
||||||
|
| `/bans` | `/players?tab=bans` |
|
||||||
|
| `/reports` | `/players?tab=reports` |
|
||||||
|
| `/gift`, `/passwords`, `/grants` | `/players` |
|
||||||
|
| `/audit` | `/activity?tab=audit` |
|
||||||
|
| `/logs` | `/activity?tab=logs` |
|
||||||
|
| `/rr-originals`, `/instances`, `/leaderboards` | `/rooms` |
|
||||||
|
| `/community` | `/content?tab=community` |
|
||||||
|
| `/loading-tips` | `/content?tab=tips` |
|
||||||
|
| `/signup-codes` | `/settings?tab=signup` |
|
||||||
|
|
||||||
|
`/import-room-legacy` still exists for the legacy room importer.
|
||||||
|
|
||||||
|
## Build And Deploy
|
||||||
|
|
||||||
|
The service Dockerfile builds the admin SPA for service images that need
|
||||||
|
static assets. In the microservices stack the `web` image must include:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/app/wwwroot/admin/index.html
|
||||||
|
/app/wwwroot/admin/assets/*
|
||||||
|
```
|
||||||
|
|
||||||
|
The admin SPA calls relative URLs, so it does not need a configured API
|
||||||
|
origin as long as the browser is opened on `https://admin.<domain>/`.
|
||||||
|
|
||||||
|
The native admin mobile app points at the same public admin host; see
|
||||||
|
[`../DorkNet.AdminMobile/README.md`](../DorkNet.AdminMobile/README.md).
|
||||||
|
|
||||||
|
## Smoke Checks
|
||||||
|
|
||||||
|
Check the static host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://admin.yourdomain.com/
|
||||||
|
```
|
||||||
|
|
||||||
|
Check that protected admin API routing is alive. A `401` is expected
|
||||||
|
without a valid admin token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i https://admin.yourdomain.com/api/admin/v1/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the built assets inside the running `web` container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec <web-container> sh -lc 'find /app/wwwroot/admin -maxdepth 2 -type f | sort | head -50'
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs <stack>-gateway-1 --tail=200
|
||||||
|
docker logs <stack>-web-1 --tail=200
|
||||||
|
docker logs <stack>-moderation-1 --tail=200
|
||||||
|
```
|
||||||
|
|
||||||
|
For normal browser admin traffic, start with `gateway` and `web`.
|
||||||
|
Check `moderation` when you are testing `/api/admin/*` through a
|
||||||
|
non-admin host such as `api.<domain>`.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Check |
|
||||||
|
|---|---|
|
||||||
|
| `admin.<domain>/` returns 404 | `web` logs for `probe="/app/wwwroot/admin/index.html" exists=False`; rebuild/redeploy the `web` image |
|
||||||
|
| Admin page loads but actions 404 | Gateway `/internal/routes`, then whether the request host is `admin.<domain>` or a path-routed host |
|
||||||
|
| Login or actions return 401 | Token expired, local session cleared, or the player no longer has `IsAdmin = true` |
|
||||||
|
| Browser spins forever | Open the browser network tab, then check `gateway` and `web` logs for the stuck URL |
|
||||||
|
| Upload/import fails near 100 MB | Use the chunked importer; the SPA uses 50 MB chunks to stay below Cloudflare request limits |
|
||||||
@@ -14,15 +14,39 @@ files in this folder.
|
|||||||
|
|
||||||
```
|
```
|
||||||
DorkNet.sln
|
DorkNet.sln
|
||||||
├── DorkNet.Server ASP.NET Core 9 backend — the bulk of the code
|
├── DorkNet.Server Shared ASP.NET Core backend + monolith fallback
|
||||||
├── DorkNet.Models Shared DTO + serialization types
|
├── DorkNet.Models Shared DTO + serialization types
|
||||||
└── DorkNet.ClientMod MelonLoader 6 IL2CPP mod — the client patcher
|
├── DorkNet.Contracts Shared service contracts for the service split
|
||||||
|
├── DorkNet.ServiceDefaults Shared service-host setup
|
||||||
|
├── DorkNet.Gateway Gateway/edge reverse proxy
|
||||||
|
├── DorkNet.Services.Identity Auth/accounts service host
|
||||||
|
├── DorkNet.Services.Rooms Rooms/matchmaking service host
|
||||||
|
├── DorkNet.Services.Notify Notify/chat service host
|
||||||
|
├── DorkNet.Services.Content CDN/images/photos/storage service host
|
||||||
|
├── DorkNet.Services.Social Clubs/groups/events/social service host
|
||||||
|
├── DorkNet.Services.Commerce Store/econ/inventory service host
|
||||||
|
├── DorkNet.Services.Platform Service directory/config/platform service host
|
||||||
|
├── DorkNet.Services.Moderation Bug reporting/moderation service host
|
||||||
|
├── DorkNet.Services.Web Public/admin/feed web host
|
||||||
|
└── DorkNet.ClientMod MelonLoader 6 IL2CPP mod — the client patcher
|
||||||
```
|
```
|
||||||
|
|
||||||
| Project | Responsibility | Target |
|
| Project | Responsibility | Target |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `DorkNet.Server` | REST API, SignalR hub, admin SPA host, static-site host, image CDN, matchmaking, persistence | .NET 9 |
|
| `DorkNet.Server` | Shared REST/controller/service stack, database bootstrap, standalone fallback, monolith fallback | .NET 9 |
|
||||||
| `DorkNet.Models` | DTOs the wire protocol speaks. No project dependencies. | .NET 9 |
|
| `DorkNet.Models` | DTOs the wire protocol speaks. No project dependencies. | .NET 9 |
|
||||||
|
| `DorkNet.Contracts` | Service names, route ownership, service-map options, health responses, probe responses | .NET 9 |
|
||||||
|
| `DorkNet.ServiceDefaults` | Common service health checks, JSON defaults, HTTP clients, service identity, route guard | .NET 9 |
|
||||||
|
| `DorkNet.Gateway` | Edge reverse proxy; routes host/path slices to services and exposes service health probes | .NET 9 |
|
||||||
|
| `DorkNet.Services.Identity` | Accounts, auth, platform login, JWT issuance route slice | .NET 9 |
|
||||||
|
| `DorkNet.Services.Rooms` | Rooms, room keys, matchmaking, discovery route slice | .NET 9 |
|
||||||
|
| `DorkNet.Services.Notify` | SignalR edge, messages/chat, notification fan-out route slice | .NET 9 |
|
||||||
|
| `DorkNet.Services.Content` | CDN paths, image/photo APIs, uploads, S3-backed blobs | .NET 9 |
|
||||||
|
| `DorkNet.Services.Social` | Clubs, groups, announcements, player events, subscriptions | .NET 9 |
|
||||||
|
| `DorkNet.Services.Commerce` | Catalog, storefronts, econ, inventory, inventions | .NET 9 |
|
||||||
|
| `DorkNet.Services.Platform` | Service URLs, config, version checks, geo, strings, telemetry-style surfaces | .NET 9 |
|
||||||
|
| `DorkNet.Services.Moderation` | Bug reporting, player reporting, sanitize, path-routed admin API, testcase routes | .NET 9 |
|
||||||
|
| `DorkNet.Services.Web` | Public site, admin static host, same-origin admin browser API, feed static host, site API | .NET 9 |
|
||||||
| `DorkNet.ClientMod` | MelonLoader 0.6.x IL2CPP mod — the client patcher, JSON config | .NET 6 |
|
| `DorkNet.ClientMod` | MelonLoader 0.6.x IL2CPP mod — the client patcher, JSON config | .NET 6 |
|
||||||
|
|
||||||
`ClientMod` applies the client-side patches needed to point the 2020
|
`ClientMod` applies the client-side patches needed to point the 2020
|
||||||
@@ -48,9 +72,57 @@ DorkNet.Server/
|
|||||||
├── admin-ui/ Vite + React SPA source — builds to wwwroot/admin/
|
├── admin-ui/ Vite + React SPA source — builds to wwwroot/admin/
|
||||||
├── site/ Public website Vite SPA source — builds to wwwroot/site/
|
├── site/ Public website Vite SPA source — builds to wwwroot/site/
|
||||||
├── wwwroot/ Static file root, with per-host subdirs (admin/, site/, feed/)
|
├── wwwroot/ Static file root, with per-host subdirs (admin/, site/, feed/)
|
||||||
└── Program.cs Host + DI + middleware pipeline (split candidates noted at the bottom)
|
├── Startup/ Host composition, DI registration, database bootstrap, middleware pipeline
|
||||||
|
└── Program.cs Short composition root
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Runtime topology
|
||||||
|
|
||||||
|
The production runtime is the gateway-fronted service split.
|
||||||
|
`DorkNet.Server` remains available as the shared compatibility runtime
|
||||||
|
and as a standalone debug/fallback image. In the compose stack it runs as
|
||||||
|
`monolith` behind the gateway for route families that have not been
|
||||||
|
assigned to a dedicated service yet.
|
||||||
|
|
||||||
|
The service split works like this:
|
||||||
|
|
||||||
|
- `DorkNet.Gateway` reverse-proxies public requests by host/path.
|
||||||
|
- `DorkNet.Services.Identity`, `DorkNet.Services.Rooms`,
|
||||||
|
`DorkNet.Services.Notify`, `DorkNet.Services.Content`,
|
||||||
|
`DorkNet.Services.Social`, `DorkNet.Services.Commerce`,
|
||||||
|
`DorkNet.Services.Platform`, `DorkNet.Services.Moderation`, and
|
||||||
|
`DorkNet.Services.Web` run the shared server stack behind a route
|
||||||
|
ownership guard. That keeps response shapes identical while moving
|
||||||
|
traffic by domain/path slice.
|
||||||
|
- `DorkNet.Gateway` exposes `/internal/services` and
|
||||||
|
`/internal/services/health`, plus `/internal/routes` for the active
|
||||||
|
proxy table.
|
||||||
|
- `DorkNet.Services.Identity` exposes
|
||||||
|
`/internal/identity/capabilities`.
|
||||||
|
- `DorkNet.Services.Rooms` exposes `/internal/rooms/capabilities`.
|
||||||
|
- `DorkNet.Services.Notify` exposes `/internal/notify/capabilities`.
|
||||||
|
- The other slices expose matching `/internal/{service}/capabilities`
|
||||||
|
endpoints.
|
||||||
|
|
||||||
|
`docker-compose.microservices.yml` starts gateway, all dedicated service
|
||||||
|
slices, the monolith fallback, and Compose-managed Postgres and Redis for
|
||||||
|
local testing. `docker-compose.microservices.dokploy.yml` is the Dokploy
|
||||||
|
production shape: it adds a `cloudflared` sidecar that joins the same
|
||||||
|
Compose network and forwards Cloudflare Tunnel traffic to
|
||||||
|
`http://gateway:8080`. Object storage is external S3-compatible storage
|
||||||
|
configured with `DORKNET_S3_*` environment variables; the compose files
|
||||||
|
do not run MinIO or Garage.
|
||||||
|
|
||||||
|
`Dockerfile.service` is the reusable service image. It accepts
|
||||||
|
`PROJECT_PATH` and `APP_DLL` build args, rebuilds the admin/site Vite
|
||||||
|
SPAs for service images that need static assets, publishes the selected
|
||||||
|
.NET project, and runs that DLL in the runtime image. The root
|
||||||
|
`Dockerfile` builds only the standalone/fallback `DorkNet.Server` image.
|
||||||
|
|
||||||
|
For the admin browser console specifically, including the host routing
|
||||||
|
split between the admin static host and `/api/admin/v1`, see
|
||||||
|
[`admin.md`](admin.md).
|
||||||
|
|
||||||
### Controllers/ groupings
|
### Controllers/ groupings
|
||||||
|
|
||||||
20 top-level groups, each responsible for one product surface the
|
20 top-level groups, each responsible for one product surface the
|
||||||
@@ -190,11 +262,14 @@ Two providers, switched by `Database:Provider` config:
|
|||||||
- **SQLite** — default for local dev. File at `bin/<config>/data/dorknet.db`.
|
- **SQLite** — default for local dev. File at `bin/<config>/data/dorknet.db`.
|
||||||
- **Postgres** — production. Connection string from `ConnectionStrings:Default`.
|
- **Postgres** — production. Connection string from `ConnectionStrings:Default`.
|
||||||
|
|
||||||
Schema lives in `Data/Entities/*` and `Data/DorkNetDbContext.cs`. EF
|
Schema lives in `Data/Entities/*` and `Data/DorkNetDbContext.cs`.
|
||||||
migrations under `Data/Migrations/` apply via `Database.Migrate()` on
|
SQLite dev DBs apply EF migrations under `Data/Migrations/` via
|
||||||
boot. Data transforms that can't be expressed as schema migrations
|
`Database.Migrate()`. The production Postgres path still uses
|
||||||
(seed renames, computed backfills) go in `Data/LegacyUpgrades.cs` as
|
`EnsureCreated()` plus idempotent bootstrap work under the same
|
||||||
idempotent methods registered in `RunAsync()`. See
|
advisory-lock discipline. Data transforms that can't be expressed as
|
||||||
|
schema migrations (seed renames, computed backfills) go in
|
||||||
|
`Data/LegacyUpgrades.cs` as idempotent methods registered in `RunAsync()`.
|
||||||
|
See
|
||||||
[`Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md) for the
|
[`Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md) for the
|
||||||
discipline: one release = one migration; everything else is
|
discipline: one release = one migration; everything else is
|
||||||
`LegacyUpgrades`.
|
`LegacyUpgrades`.
|
||||||
@@ -216,9 +291,11 @@ If you've never touched this codebase, the most approachable path:
|
|||||||
4. Look at one **`Cpp2IL`-cited line** in a controller to see how
|
4. Look at one **`Cpp2IL`-cited line** in a controller to see how
|
||||||
wire shapes are pinned to decompiled provenance.
|
wire shapes are pinned to decompiled provenance.
|
||||||
|
|
||||||
When in doubt: open `DorkNet.Server/Program.cs` and the
|
When in doubt: open `DorkNet.Contracts/DorkNetRouteOwnership.cs`,
|
||||||
|
`DorkNet.Gateway/Program.cs`, `DorkNet.Server/Program.cs`, and the
|
||||||
`Versions/Late2020/Late2020VersionPlugin.cs` summary doc-comment.
|
`Versions/Late2020/Late2020VersionPlugin.cs` summary doc-comment.
|
||||||
Between them they explain 80% of the cross-cutting decisions.
|
Together they explain route ownership, proxying, the shared server
|
||||||
|
pipeline, and version compatibility.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -236,11 +313,11 @@ worse.
|
|||||||
intentional — the decompiled source is provenance only, not
|
intentional — the decompiled source is provenance only, not
|
||||||
shippable. Comments give a future contributor a breadcrumb to
|
shippable. Comments give a future contributor a breadcrumb to
|
||||||
re-derive the wire shape if the decompile changes.
|
re-derive the wire shape if the decompile changes.
|
||||||
- **`Program.cs` is long.** Most of it is one-time schema patching
|
- **Microservices use shared server code during extraction.** The
|
||||||
(`RunPatchAsync` calls for the Postgres `EnsureCreated` path) that's
|
dedicated service hosts run the proven controller/service stack behind
|
||||||
pending consolidation per [`Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md).
|
route guards instead of copying controllers into new projects. This
|
||||||
Once that lands, the remaining ~500 lines of middleware + host
|
keeps the client contract stable while route families are peeled away
|
||||||
setup will be split into `Startup/*` extensions.
|
from the monolith fallback.
|
||||||
- **Two-provider EF.** SQLite and Postgres each have their own
|
- **Two-provider EF.** SQLite and Postgres each have their own
|
||||||
EnsureCreated/Migrate dance because some Postgres-only column types
|
EnsureCreated/Migrate dance because some Postgres-only column types
|
||||||
drift the model snapshot. The split is annoying but lets one
|
drift the model snapshot. The split is annoying but lets one
|
||||||
|
|||||||
@@ -1,280 +1,439 @@
|
|||||||
# DorkNet — Coolify deployment
|
# DorkNet Deployment
|
||||||
|
|
||||||
This doc covers the production cutover from the local-machine setup
|
The recommended production shape is the gateway-fronted microservices
|
||||||
(SQLite + Kestrel direct + mkcert + per-machine cloudflared) to the
|
compose stack. The standalone `DorkNet.Server` image still exists for
|
||||||
horizontally-scaled Coolify deployment (Postgres + Redis + Garage S3 +
|
local debugging and emergency single-container fallback, but new Dokploy
|
||||||
2-replica Kestrel behind cloudflared, all on the same Coolify project).
|
deployments should use `docker-compose.microservices.dokploy.yml`.
|
||||||
|
|
||||||
The plan-of-record is `BUILD_PROPER_PLAN.md` at the repo root and the
|
Object storage is always a separate S3-compatible service. The compose
|
||||||
phased writeup at `~/.claude/plans/pahese-0-1-horizontal-cheeky-hickey.md`.
|
files start Postgres and Redis, but they do not start MinIO, Garage, or
|
||||||
Anything that contradicts this doc, that doc wins.
|
any other object-storage emulator.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Coolify project layout
|
## Production Topology
|
||||||
|
|
||||||
One Coolify *project* with these services:
|
Public traffic enters at `DorkNet.Gateway`. The gateway preserves the
|
||||||
|
original Host header, routes owned host/path families to dedicated
|
||||||
|
service hosts, and sends unassigned route families to the `monolith`
|
||||||
|
fallback. That lets the 2020 client keep using the same public URLs
|
||||||
|
while route slices move out of the fallback server.
|
||||||
|
|
||||||
| Service | Image / source | Notes |
|
| Service | Purpose |
|
||||||
| -------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------- |
|
|---|---|
|
||||||
| `dorknet-server` | Build from this repo's `Dockerfile` | Replicas: **2** to start. Internal port 8080. No public domain. |
|
| `gateway` | Edge reverse proxy, service map, route table, and service health probes |
|
||||||
| `dorknet-postgres` | Coolify one-click **Postgres** | Persistent volume on Coolify's data store. |
|
| `identity` | Auth, accounts, platform login, JWT issuance |
|
||||||
| `dorknet-redis` | Coolify one-click **Redis** | Persistent volume optional; used for ephemeral state + SignalR backplane. |
|
| `rooms` | Rooms, room keys, playlists, matchmaking, discovery |
|
||||||
| `dorknet-garage` | Coolify one-click **Garage** | S3-compatible. Persistent volume (room blobs are 100s of MB). |
|
| `notify` | Notify, messages/chat, SignalR, notification fan-out |
|
||||||
| `dorknet-cloudflared`| Cloudflare's `cloudflared` image, custom config | Reads `tools/cloudflared-config.coolify.yml.template` rendered with a tunnel ID. |
|
| `content` | CDN paths, uploads, images, photos, room blobs, storage APIs |
|
||||||
|
| `social` | Clubs, groups, announcements, player events, subscriptions |
|
||||||
|
| `commerce` | Catalog, storefronts, econ, inventory, inventions |
|
||||||
|
| `platform` | Service directory, config, version checks, geo, strings |
|
||||||
|
| `moderation` | Bug reports, player reports, sanitize, path-routed admin API, testcase routes |
|
||||||
|
| `web` | Apex/www/admin/feed static hosts, same-origin admin browser API, and site API |
|
||||||
|
| `monolith` | Fallback shared server for route families not split yet |
|
||||||
|
| `postgres` | Bundled Postgres for the compose network |
|
||||||
|
| `redis` | Bundled Redis for ephemeral state and fan-out |
|
||||||
|
| `cloudflared` | Cloudflare Tunnel sidecar in the Dokploy compose file |
|
||||||
|
|
||||||
All five sit on the same project network so the server reaches its
|
Every backend service uses the same database, Redis, S3, Photon, domain,
|
||||||
dependencies over Coolify's internal Docker DNS.
|
JWT, and version-gate settings. The dedicated slices currently reuse the
|
||||||
|
shared `DorkNet.Server` controller/service stack behind route ownership
|
||||||
|
guards, so response contracts stay identical while the architecture is
|
||||||
|
split.
|
||||||
|
|
||||||
## Required environment variables on `dorknet-server`
|
---
|
||||||
|
|
||||||
Set these in Coolify's *Environment Variables* tab. Coolify expands
|
## Dokploy Microservices
|
||||||
`${...}` references to other services in the same project, so the
|
|
||||||
connection-string lookups link automatically.
|
|
||||||
|
|
||||||
|
Create a Dokploy **Compose** service from:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docker-compose.microservices.dokploy.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use the GitHub repo/branch as the source and keep the build context at
|
||||||
|
the repository root. Do not add Dokploy domain rows for this compose
|
||||||
|
stack when using the Cloudflare Tunnel sidecar; public hostname routing
|
||||||
|
belongs to Cloudflare.
|
||||||
|
|
||||||
|
Set these Dokploy Compose environment variables:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DORKNET_DOMAIN=yourdomain.com
|
||||||
|
DORKNET_JWT_SECRET=replace-with-at-least-64-random-characters
|
||||||
|
CLOUDFLARE_TUNNEL_TOKEN=<token from Cloudflare Zero Trust tunnel>
|
||||||
|
|
||||||
|
# Bundled compose Postgres. Optional when DORKNET_POSTGRES_CONNECTION_STRING is set.
|
||||||
|
POSTGRES_PASSWORD=replace-with-a-random-db-password
|
||||||
|
|
||||||
|
# External object storage. Required for production.
|
||||||
|
DORKNET_S3_ENDPOINT=https://your-s3-api-endpoint
|
||||||
|
DORKNET_S3_ACCESS_KEY=your-access-key
|
||||||
|
DORKNET_S3_SECRET_KEY=your-secret-key
|
||||||
|
DORKNET_S3_REGION=garage
|
||||||
|
|
||||||
|
# Photon Cloud.
|
||||||
|
Photon__AppId=your-photon-realtime-app-id
|
||||||
|
Photon__VoiceAppId=your-photon-voice-app-id
|
||||||
|
Photon__CloudRegion=eu
|
||||||
|
|
||||||
|
# December 2020 client version gate.
|
||||||
|
DORKNET_DEFAULT_CLIENT_VERSION=december_2020_12_18
|
||||||
|
DORKNET_SUPPORTED_VERSION=december_2020_12_18
|
||||||
|
DORKNET_DECEMBER_BUILD_VERSION_KEY=december_2020_12_18
|
||||||
|
```
|
||||||
|
|
||||||
|
The compose file maps the `DORKNET_S3_*` values into `S3__*` for every
|
||||||
|
backend service, and maps the version variables into `DorkNet__*`
|
||||||
|
configuration keys. `Photon__*` is passed directly to each service.
|
||||||
|
|
||||||
|
### Database Options
|
||||||
|
|
||||||
|
By default, service containers connect to the bundled compose Postgres:
|
||||||
|
|
||||||
|
```env
|
||||||
|
Host=postgres;Port=5432;Database=dorknet;Username=dorknet;Password=${POSTGRES_PASSWORD}
|
||||||
|
```
|
||||||
|
|
||||||
|
To use a Dokploy-managed or external Postgres instead, set a full
|
||||||
|
connection string:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DORKNET_POSTGRES_CONNECTION_STRING=Host=<postgres-host>;Port=5432;Database=dorknet;Username=<user>;Password=<password>
|
||||||
|
```
|
||||||
|
|
||||||
|
For hosted Postgres that requires TLS:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DORKNET_POSTGRES_CONNECTION_STRING=Host=<postgres-host>;Port=5432;Database=dorknet;Username=<user>;Password=<password>;SSL Mode=Require;Trust Server Certificate=true
|
||||||
|
```
|
||||||
|
|
||||||
|
`docker-compose.microservices.dokploy.yml` attaches every backend service
|
||||||
|
that opens database connections to both the stack's default network and
|
||||||
|
Dokploy's external `dokploy-network`. If you use another Dokploy-managed
|
||||||
|
database service, the hostname in `DORKNET_POSTGRES_CONNECTION_STRING`
|
||||||
|
must resolve to a Docker-network address from inside a DorkNet service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec <web-or-platform-container> getent hosts <postgres-host>
|
||||||
|
```
|
||||||
|
|
||||||
|
That should return a container/network IP, not `127.0.0.1`.
|
||||||
|
|
||||||
|
If you use the bundled compose Postgres, `POSTGRES_PASSWORD` must match
|
||||||
|
the password that initialized the existing Postgres volume. Changing
|
||||||
|
`POSTGRES_PASSWORD` later does not update the already-created `dorknet`
|
||||||
|
database user. Restore the original password, reset it inside Postgres,
|
||||||
|
or recreate the Postgres volume.
|
||||||
|
|
||||||
|
### Cloudflare Tunnel
|
||||||
|
|
||||||
|
In Cloudflare Zero Trust, configure two public hostnames:
|
||||||
|
|
||||||
|
```text
|
||||||
|
yourdomain.com -> http://gateway:8080
|
||||||
|
*.yourdomain.com -> http://gateway:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
The wildcard covers `api`, `auth`, `rooms`, `notify`, `cdn`, `clubs`,
|
||||||
|
`commerce`, and the other client-facing subdomains. The apex rule is
|
||||||
|
separate because wildcard DNS does not cover the root domain. Let
|
||||||
|
Cloudflare create the tunnel DNS records, or create CNAME records to the
|
||||||
|
tunnel target Cloudflare gives you.
|
||||||
|
|
||||||
|
Do not point the tunnel at individual service containers. All public
|
||||||
|
traffic should go to `http://gateway:8080`.
|
||||||
|
|
||||||
|
### Public Hosts
|
||||||
|
|
||||||
|
The client service map and the browser surfaces expect these hosts to
|
||||||
|
reach the gateway:
|
||||||
|
|
||||||
|
```text
|
||||||
|
yourdomain.com
|
||||||
|
www.yourdomain.com
|
||||||
|
admin.yourdomain.com
|
||||||
|
api.yourdomain.com
|
||||||
|
auth.yourdomain.com
|
||||||
|
accounts.yourdomain.com
|
||||||
|
bugreporting.yourdomain.com
|
||||||
|
cards.yourdomain.com
|
||||||
|
cdn.yourdomain.com
|
||||||
|
chat.yourdomain.com
|
||||||
|
clubs.yourdomain.com
|
||||||
|
cms.yourdomain.com
|
||||||
|
commerce.yourdomain.com
|
||||||
|
data.yourdomain.com
|
||||||
|
datacollection.yourdomain.com
|
||||||
|
discovery.yourdomain.com
|
||||||
|
econ.yourdomain.com
|
||||||
|
feed.yourdomain.com
|
||||||
|
gamelogs.yourdomain.com
|
||||||
|
geo.yourdomain.com
|
||||||
|
img.yourdomain.com
|
||||||
|
leaderboard.yourdomain.com
|
||||||
|
link.yourdomain.com
|
||||||
|
lists.yourdomain.com
|
||||||
|
match.yourdomain.com
|
||||||
|
moderation.yourdomain.com
|
||||||
|
notify.yourdomain.com
|
||||||
|
ns.yourdomain.com
|
||||||
|
platformnotifications.yourdomain.com
|
||||||
|
playersettings.yourdomain.com
|
||||||
|
roomcomments.yourdomain.com
|
||||||
|
roomieintegrations.yourdomain.com
|
||||||
|
rooms.yourdomain.com
|
||||||
|
storage.yourdomain.com
|
||||||
|
strings.yourdomain.com
|
||||||
|
strings-cdn.yourdomain.com
|
||||||
|
studio.yourdomain.com
|
||||||
|
thorn.yourdomain.com
|
||||||
|
videos.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
With Cloudflare Tunnel, the apex and wildcard hostname rules cover the
|
||||||
|
list. If you deploy without a tunnel, every host must route to the
|
||||||
|
gateway container on port `8080`.
|
||||||
|
|
||||||
|
### Smoke Checks
|
||||||
|
|
||||||
|
After Dokploy finishes rebuilding, verify the gateway and service map:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://api.yourdomain.com/healthz
|
||||||
|
curl https://api.yourdomain.com/internal/services
|
||||||
|
curl https://api.yourdomain.com/internal/services/health
|
||||||
|
curl https://api.yourdomain.com/internal/routes
|
||||||
|
curl https://api.yourdomain.com/api/versioncheck/v4?v=20201210
|
||||||
|
```
|
||||||
|
|
||||||
|
The version check should return:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"VersionStatus":0}
|
||||||
|
```
|
||||||
|
|
||||||
|
Browser hosts should route to `web` and return static files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://yourdomain.com/
|
||||||
|
curl -I https://admin.yourdomain.com/
|
||||||
|
curl -I https://feed.yourdomain.com/
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the running web container, these files must exist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec <web-container> sh -lc 'ls -la /app/wwwroot/admin /app/wwwroot/site /app/wwwroot/feed'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected key files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/app/wwwroot/admin/index.html
|
||||||
|
/app/wwwroot/site/index.html
|
||||||
|
/app/wwwroot/feed/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
`/internal/routes` shows which host/path patterns route to each service
|
||||||
|
slice or to the monolith fallback. `/internal/services/health` reports
|
||||||
|
gateway-visible health for each backend service.
|
||||||
|
|
||||||
|
For the admin page's browser routes, auth flow, same-origin API calls,
|
||||||
|
and focused troubleshooting, see [`admin.md`](admin.md).
|
||||||
|
|
||||||
|
### Useful Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs <stack>-gateway-1 --tail=200
|
||||||
|
docker logs <stack>-web-1 --tail=200
|
||||||
|
docker logs <stack>-moderation-1 --tail=200
|
||||||
|
docker logs <stack>-platform-1 --tail=200
|
||||||
|
docker logs <stack>-cloudflared-1 --tail=200
|
||||||
|
```
|
||||||
|
|
||||||
|
If `admin.<domain>` or the apex returns 404, check the `web` logs for
|
||||||
|
static-host probes such as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
probe="/app/wwwroot/admin/index.html" exists=False
|
||||||
|
```
|
||||||
|
|
||||||
|
That means the `web` image did not include the built SPA assets or the
|
||||||
|
old container is still running. Force a clean Dokploy rebuild on the
|
||||||
|
latest branch commit.
|
||||||
|
|
||||||
|
Admin browser traffic on `admin.<domain>` is routed to `web`. The
|
||||||
|
`moderation` service is still useful when testing the `/api/admin/*`
|
||||||
|
path family through non-admin hosts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Local Microservices
|
||||||
|
|
||||||
|
For local service-split testing without Dokploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.microservices.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the gateway on host port `8080`, all service slices, the
|
||||||
|
monolith fallback, Postgres, and Redis. The local compose file does not
|
||||||
|
start `cloudflared`; direct local testing uses Host headers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H 'Host: api.localhost' http://localhost:8080/healthz
|
||||||
|
curl -H 'Host: admin.localhost' http://localhost:8080/
|
||||||
|
curl -H 'Host: localhost' http://localhost:8080/
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `DORKNET_S3_*` if you need blob/image features locally. Leaving S3
|
||||||
|
blank is fine for API smoke tests that do not touch object storage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Standalone Server Fallback
|
||||||
|
|
||||||
|
The root `Dockerfile` builds the standalone `DorkNet.Server` image. Use
|
||||||
|
it for local debugging, narrow rollback tests, or a temporary
|
||||||
|
single-container deployment. The current production docs and Dokploy
|
||||||
|
workflow assume the microservices compose stack instead.
|
||||||
|
|
||||||
|
Create a Dokploy **Application** only if you intentionally want the
|
||||||
|
single-container fallback:
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Source | This GitHub repo / branch |
|
||||||
|
| Build type | Dockerfile |
|
||||||
|
| Dockerfile path | `Dockerfile` |
|
||||||
|
| Context | `.` |
|
||||||
|
| Container port | `8080` |
|
||||||
|
|
||||||
|
Standalone environment variables use the direct .NET configuration keys:
|
||||||
|
|
||||||
|
```env
|
||||||
ASPNETCORE_ENVIRONMENT=Production
|
ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ASPNETCORE_URLS=http://0.0.0.0:8080
|
||||||
|
DOTNET_RUNNING_IN_CONTAINER=true
|
||||||
|
|
||||||
# Database
|
DORKNET_DOMAIN=yourdomain.com
|
||||||
ConnectionStrings__Default=Host=dorknet-postgres;Port=5432;Database=dorknet;Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}
|
DORKNET_JWT_SECRET=replace-with-at-least-64-random-characters
|
||||||
|
|
||||||
# Redis
|
Database__Provider=postgres
|
||||||
ConnectionStrings__Redis=dorknet-redis:6379
|
ConnectionStrings__Default=Host=<postgres-host>;Port=5432;Database=dorknet;Username=<user>;Password=<password>
|
||||||
|
ConnectionStrings__Redis=redis://<redis-host>:6379
|
||||||
|
|
||||||
# JWT
|
S3__Endpoint=https://your-s3-api-endpoint
|
||||||
DORKNET_JWT_SECRET=<openssl rand -base64 64>
|
S3__AccessKey=your-access-key
|
||||||
|
S3__SecretKey=your-secret-key
|
||||||
# Object storage (Garage). Bucket names are hard-coded in code; only
|
|
||||||
# the endpoint + credentials need configuring. Objects are organized
|
|
||||||
# under owner-scoped keys inside each bucket, e.g.
|
|
||||||
# players/{playerId}/camera, players/{playerId}/profile,
|
|
||||||
# players/{playerId}/rooms/{roomId}, players/{playerId}/dorm.
|
|
||||||
S3__Endpoint=http://dorknet-garage:3900
|
|
||||||
S3__Region=garage
|
S3__Region=garage
|
||||||
S3__AccessKey=<garage admin key>
|
|
||||||
S3__SecretKey=<garage admin secret>
|
|
||||||
|
|
||||||
# Image CDN signing. The 2020 client verifies Content-Signature on
|
Photon__AppId=your-photon-realtime-app-id
|
||||||
# image downloads. Use the private half of the key whose public modulus
|
Photon__VoiceAppId=your-photon-voice-app-id
|
||||||
# the client knows for p1. Store PEM newlines as \n if your env UI is
|
Photon__CloudRegion=eu
|
||||||
# single-line, or use DORKNET_IMAGE_SIGNING_PRIVATE_KEY_BASE64.
|
DorkNet__DefaultClientVersion=december_2020_12_18
|
||||||
DORKNET_IMAGE_SIGNING_KEY_ID=p1
|
DorkNet__SupportedVersions__0=december_2020_12_18
|
||||||
DORKNET_IMAGE_SIGNING_PRIVATE_KEY=<PEM RSA private key>
|
DorkNet__BuildIdToVersionKey__20201210=december_2020_12_18
|
||||||
|
|
||||||
# Photon (copy from local appsettings.json — same AppId across envs)
|
|
||||||
Photon__AppId=<your-photon-realtime-app-id>
|
|
||||||
Photon__VoiceAppId=1917ff4a-b7ad-4ad6-9ea2-9642bc174b70
|
|
||||||
Photon__CloudRegion=us
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cloudflare R2 instead of Garage
|
Every public host listed above must route to this single container when
|
||||||
|
running standalone.
|
||||||
|
|
||||||
DorkNet can use Cloudflare R2 without code changes because R2 speaks the
|
---
|
||||||
S3-compatible API. Create these three R2 buckets first:
|
|
||||||
|
|
||||||
```
|
## S3 Storage
|
||||||
|
|
||||||
|
DorkNet uses S3-compatible storage for room blobs, profile images, camera
|
||||||
|
photos, and CDN/image pipeline backing data. Create these buckets before
|
||||||
|
production cutover:
|
||||||
|
|
||||||
|
```text
|
||||||
profile-images
|
profile-images
|
||||||
camera-photos
|
camera-photos
|
||||||
room-blobs
|
room-blobs
|
||||||
```
|
```
|
||||||
|
|
||||||
Then create an R2 API token with Object Read & Write access to both
|
Garage-style endpoints usually use:
|
||||||
buckets and set the server env vars:
|
|
||||||
|
|
||||||
|
```env
|
||||||
|
S3__Endpoint=https://your-garage-or-s3-api
|
||||||
|
S3__Region=garage
|
||||||
|
S3__AccessKey=...
|
||||||
|
S3__SecretKey=...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Cloudflare R2 uses the S3 API endpoint, not a public bucket URL:
|
||||||
|
|
||||||
|
```env
|
||||||
S3__Endpoint=https://<cloudflare-account-id>.r2.cloudflarestorage.com
|
S3__Endpoint=https://<cloudflare-account-id>.r2.cloudflarestorage.com
|
||||||
S3__Region=auto
|
S3__Region=auto
|
||||||
S3__AccessKey=<r2 access key id>
|
S3__AccessKey=<r2 access key id>
|
||||||
S3__SecretKey=<r2 secret access key>
|
S3__SecretKey=<r2 secret access key>
|
||||||
S3__MaxErrorRetry=1
|
S3__MaxErrorRetry=1
|
||||||
S3__TimeoutSeconds=8
|
S3__TimeoutSeconds=300
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not use an R2 public bucket URL or custom domain for `S3__Endpoint`.
|
For the microservices compose files, use the `DORKNET_S3_*`
|
||||||
That value must be the S3 API endpoint ending in
|
equivalents; compose maps them into `S3__*` for the service containers.
|
||||||
`.r2.cloudflarestorage.com`.
|
|
||||||
DorkNet disables S3 chunked upload encoding in code because R2 rejects
|
|
||||||
`STREAMING-AWS4-HMAC-SHA256-PAYLOAD` request bodies.
|
|
||||||
|
|
||||||
`appsettings.Production.json` already pins `Database:Provider=postgres`
|
---
|
||||||
and binds Kestrel to `http://0.0.0.0:8080`, so those don't need to be
|
|
||||||
set as env vars.
|
|
||||||
|
|
||||||
## First-time cutover (stop-the-world)
|
## First-Time Cutover From SQLite
|
||||||
|
|
||||||
1. **Drain the local server.** Tell players, take it down. The cutover
|
1. Stop the local server so the SQLite source stops accepting writes.
|
||||||
is non-incremental — once the SQLite source is read, no further
|
2. Snapshot the SQLite database.
|
||||||
writes can be accepted there.
|
3. Provision Postgres, Redis, and separate S3-compatible storage.
|
||||||
|
4. Run the migration/import tooling against the new Postgres and S3
|
||||||
|
targets.
|
||||||
|
5. Deploy the microservices compose stack.
|
||||||
|
6. Verify `/healthz`, `/internal/services/health`, versioncheck, and
|
||||||
|
the browser static hosts before sending players to the new host.
|
||||||
|
|
||||||
2. **Snapshot SQLite.** `tools/snapshot-db.ps1` writes a timestamped
|
The database bootstrap path uses advisory locking so multiple service
|
||||||
copy. Keep it indefinitely as the pre-cutover baseline.
|
containers can start without racing schema/bootstrap work.
|
||||||
|
|
||||||
3. **Deploy the Coolify project** with the four services above
|
Rollback before players write to Postgres is simple: stop the deploy and
|
||||||
(Postgres / Redis / Garage / cloudflared) but the server replicas
|
return to the SQLite snapshot. After players write to Postgres, rollback
|
||||||
set to 0 — we want the database empty and idle while the migrator
|
means losing post-cutover writes unless you migrate them back.
|
||||||
runs.
|
|
||||||
|
|
||||||
4. **Run the migrator** from the operator's laptop, pointed at the
|
---
|
||||||
newly-provisioned Postgres + Garage:
|
|
||||||
|
|
||||||
```pwsh
|
## Photon Cloud
|
||||||
dotnet run --project Tools/MigrateSqliteToPostgres -- `
|
|
||||||
--sqlite ./snapshots/dorknet-pre-cutover.db `
|
|
||||||
--postgres "Host=<coolify-postgres-public-host>;Port=5432;Database=dorknet;Username=...;Password=...;SSL Mode=Require" `
|
|
||||||
--reset `
|
|
||||||
--upload-blobs `
|
|
||||||
--s3-endpoint http://<coolify-garage-public-host>:3900 `
|
|
||||||
--s3-bucket room-blobs `
|
|
||||||
--s3-access-key ... `
|
|
||||||
--s3-secret-key ...
|
|
||||||
```
|
|
||||||
|
|
||||||
The migrator runs the 9-step verification + an optional 10th step
|
Use your own Photon AppIds. Rec Room's original Photon AppIds reject
|
||||||
that bulk-uploads every `RoomDataBlobs.Bytes` row to S3, idempotent
|
external clients because their dashboard points CustomAuth at the
|
||||||
on re-run. A non-zero exit means **abort**: leave the destination
|
official backend.
|
||||||
in its dirty state, fix the diff, re-run with `--reset` for a clean
|
|
||||||
slate.
|
|
||||||
|
|
||||||
5. **Scale `dorknet-server` to 2 replicas.** First boot: one replica
|
1. Create a Photon Realtime app at <https://dashboard.photonengine.com>.
|
||||||
wins the `pg_advisory_xact_lock` and runs `EnsureCreated` (no-op,
|
2. Create a Photon Voice app if voice chat is needed.
|
||||||
schema already there from the migrator) + the seed steps; the other
|
3. Set:
|
||||||
blocks on the lock until the first commits, sees the schema is
|
|
||||||
complete, and proceeds.
|
|
||||||
|
|
||||||
6. **Verify `/healthz` is 200** on both replicas (Coolify shows this
|
```env
|
||||||
in the Container tab) before pointing the tunnel at the service.
|
Photon__AppId=<your realtime AppId>
|
||||||
|
Photon__VoiceAppId=<your voice AppId>
|
||||||
7. **Bring up `dorknet-cloudflared`** with the rendered config (see
|
Photon__CloudRegion=eu
|
||||||
`tools/cloudflared-config.coolify.yml.template`). Confirm `dig` /
|
|
||||||
`curl` against `https://api.localhost/api/health/v1` returns 200.
|
|
||||||
|
|
||||||
8. **Tell players it's back.** Done.
|
|
||||||
|
|
||||||
## Notes on multi-replica behaviour
|
|
||||||
|
|
||||||
* **Sticky sessions are no longer required.** Auth's
|
|
||||||
`OrphanAccountTracker` writes the pending claim to Redis, so a
|
|
||||||
player whose two HTTP requests hit different replicas still sees
|
|
||||||
the same account-creation state.
|
|
||||||
|
|
||||||
* **SignalR groups fan out across replicas.** The Redis backplane is
|
|
||||||
registered under the channel prefix `dorknet-signalr`; both replicas
|
|
||||||
publish/subscribe to the same channels, so a notification raised on
|
|
||||||
replica B reaches a player connected to replica A.
|
|
||||||
|
|
||||||
* **Player presence is shared.** `PlayerPresenceService` writes
|
|
||||||
`presence:player:{id}` keys to Redis with a 45 s TTL (3× heartbeat).
|
|
||||||
Either replica can answer "where is player N?".
|
|
||||||
|
|
||||||
* **Game session ids are allocated by Postgres.** `GameSessionService`
|
|
||||||
uses a `bigserial` so two replicas can `JoinOrCreate` simultaneously
|
|
||||||
without colliding.
|
|
||||||
|
|
||||||
* **Private instances live in Postgres.** Cross-replica visibility is
|
|
||||||
automatic — both replicas read from the same `PrivateInstances`
|
|
||||||
table.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
If the cutover goes sideways before step 5 (no replicas running yet),
|
|
||||||
just delete the Postgres / Redis / Garage volumes in Coolify, set the
|
|
||||||
server replica count back to 0, and bring the local SQLite server back
|
|
||||||
up. The pre-cutover snapshot is still untouched.
|
|
||||||
|
|
||||||
After step 5, rolling back means **restoring the SQLite snapshot**
|
|
||||||
(any data players wrote on Postgres post-cutover will be lost). Be
|
|
||||||
sure the cutover validation in step 6 is convincing before you tell
|
|
||||||
players it's safe.
|
|
||||||
|
|
||||||
## Photon Cloud — own AppId + CustomAuth callback
|
|
||||||
|
|
||||||
The hard-coded RecRoom Photon AppId rejects external clients (NameServer
|
|
||||||
error code 32736 / "No auth request during expected wait time") because
|
|
||||||
their dashboard has CustomAuth set to "Reject if Auth Failed" pointing
|
|
||||||
at RecRoom's own backend. Solution: register your own free AppId.
|
|
||||||
|
|
||||||
1. Go to https://dashboard.photonengine.com → sign up → **Create a New App**
|
|
||||||
→ "Photon Realtime" → save. Repeat for "Photon Voice" if you want voice.
|
|
||||||
2. Copy each **AppId** (UUID) and set in Coolify env vars:
|
|
||||||
```
|
|
||||||
Photon__AppId=<your realtime AppId>
|
|
||||||
Photon__VoiceAppId=<your voice AppId>
|
|
||||||
Photon__CloudRegion=us
|
|
||||||
```
|
|
||||||
3. Restart the server.
|
|
||||||
|
|
||||||
That alone gets multiplayer working — the Photon NameServer accepts the
|
|
||||||
client because no CustomAuth is required.
|
|
||||||
|
|
||||||
### (Optional) wire up server-side single-session enforcement
|
|
||||||
|
|
||||||
Server already exposes `https://auth.localhost/photon/customauth` — see
|
|
||||||
[PhotonCustomAuthController.cs](DorkNet.Server/Controllers/Auth/PhotonCustomAuthController.cs).
|
|
||||||
It accepts `userid` + `LoginLock` and validates them against
|
|
||||||
`PlayerPresenceService` (Redis). Enable it in the dashboard:
|
|
||||||
|
|
||||||
1. Photon dashboard → your app → **Manage** → **Custom Authentication** tab.
|
|
||||||
2. **Enable Custom Authentication**.
|
|
||||||
3. URL: `https://auth.localhost/photon/customauth?userid={userid}&LoginLock={LoginLock}`
|
|
||||||
4. **Reject if Auth Failed**: **OFF for now** (see caveat below).
|
|
||||||
5. Save.
|
|
||||||
|
|
||||||
**Caveat — current 2020 watch doesn't send AuthValues**. A sweep of
|
|
||||||
`Cpp2IL_ISIL/IsilDump/Assembly-CSharp/` turns up no caller of
|
|
||||||
`PhotonNetwork.set_AuthValues` outside Photon's own library code, so
|
|
||||||
the unmodified client connects without supplying `{userid, LoginLock}`.
|
|
||||||
With "Reject if Auth Failed" ON, every Photon connect would fail with
|
|
||||||
NameServer 32736 — same symptom we tried to solve. With it OFF, the
|
|
||||||
client connects fine, the endpoint exists for future client patching.
|
|
||||||
|
|
||||||
### Patching the watch to send AuthValues — `DorkNet.ClientMod/`
|
|
||||||
|
|
||||||
The vanilla 2020 watch never sets `PhotonNetwork.AuthValues` (verified
|
|
||||||
by an exhaustive grep across both `Assembly-CSharp.dll` and the
|
|
||||||
Photon `Assembly-CSharp-firstpass.dll` library). To make Photon
|
|
||||||
forward `userid` + `LoginLock` to our `/photon/customauth` endpoint a
|
|
||||||
Harmony patch on the watch's Photon auth path has to attach the values
|
|
||||||
in flight. That hook lives in the **MelonLoader IL2CPP mod**
|
|
||||||
(`DorkNet.ClientMod`); the AuthValues injector itself is currently
|
|
||||||
parked (see `DorkNet.ClientMod/attic/AuthValuesInjector.cs.attic` and
|
|
||||||
the notes in `Mod.cs`) since the `/photon/customauth` endpoint runs in
|
|
||||||
permissive mode.
|
|
||||||
|
|
||||||
Install the mod on a tester's machine with:
|
|
||||||
|
|
||||||
```pwsh
|
|
||||||
.\tools\install-melon.ps1 `
|
|
||||||
-RecRoomPath "C:\…\RecRoom\Recroom_Release_Data" `
|
|
||||||
-PhotonAppId cb0880d9-…
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The script unpacks MelonLoader, builds `DorkNet.ClientMod.dll`, and
|
Server-side Photon custom auth exists at `/photon/customauth`, but the
|
||||||
drops it into the client's `Mods/` folder. On the first run it prints
|
2020 client does not send the needed AuthValues without client patching.
|
||||||
"launch the game once" so MelonLoader can generate its IL2CPP
|
Leave Photon dashboard "Reject if Auth Failed" off unless the client mod
|
||||||
assemblies; relaunch the script with `-ResumeBuild` to finish.
|
is updated to send `userid` and `LoginLock`.
|
||||||
|
|
||||||
Once AuthValues injection is re-enabled in the mod and in production:
|
---
|
||||||
|
|
||||||
1. Flip [`PhotonCustomAuthController`](../DorkNet.Server/Controllers/Auth/PhotonCustomAuthController.cs)
|
## Future Schema Changes
|
||||||
from permissive-mode (`ResultCode 1` always) back to strict —
|
|
||||||
parse `userid` + `LoginLock`, validate against
|
|
||||||
`PlayerPresenceService.ValidateLock`, return `ResultCode 3` on
|
|
||||||
mismatch.
|
|
||||||
2. In the Photon dashboard, flip **"Reject if Auth Failed"** ON.
|
|
||||||
3. Duplicate-account login from a different LoginLock now fails
|
|
||||||
Photon auth at the NameServer — never reaches the dorm.
|
|
||||||
|
|
||||||
## Future schema changes
|
SQLite dev DBs apply EF migrations. The production Postgres path is still
|
||||||
|
based on `EnsureCreated()` plus idempotent bootstrap steps. Until that is
|
||||||
|
changed to `Migrate()` under the same advisory lock, treat production
|
||||||
|
schema changes as explicit deployment work:
|
||||||
|
|
||||||
PR 4 keeps `EnsureCreated` for the first deploy (the migrator builds
|
1. Add or update the entity model.
|
||||||
the schema directly, then EnsureCreated is a no-op the server can do
|
2. Add the migration for SQLite/dev.
|
||||||
under advisory-lock to be safe on multi-replica boot). Any
|
3. Add any needed idempotent Postgres bootstrap SQL.
|
||||||
**post-cutover** schema change needs a real Postgres migration:
|
4. Deploy and verify `/healthz` and `/internal/services/health`.
|
||||||
|
|
||||||
1. `dotnet ef migrations add <Name> --project DorkNet.Server`
|
See [`../DorkNet.Server/Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md)
|
||||||
2. Replace the `EnsureCreated` call in `Program.cs` with
|
for the detailed migration discipline.
|
||||||
`db.Database.Migrate()` *inside* the same advisory-lock block.
|
|
||||||
3. Deploy. Both replicas race for the lock, the winner runs the
|
|
||||||
migration, the loser sees the new history row and skips.
|
|
||||||
|
|
||||||
Until that swap happens, treat the schema as frozen.
|
|
||||||
|
|||||||