Compare commits

..

33 Commits

Author SHA1 Message Date
Alexa 24cc13084a Whitelist api.{apex} as a shared public host (#34)
api.{apex} is the client's primary API host, served by path
dispatch across every service. It belonged to no route group's
HostSubdomains, so it was absent from PublicSubdomains and thus
from the monolith's HostFiltering AllowedHosts — every request to
api.{apex} was rejected with 400 "Bad Request - Invalid Hostname".

Append "api" alongside admin/feed so it is whitelisted while
remaining path-routed (it owns no service by host).
2026-06-30 13:35:42 +03:00
Alexa 193d9b343c Matchmade sessions respect room's subroom Max Player Count (#35)
* Avoid login kick broadcast during reconnect

* feat: Matchmade sessions respect room's subroom Max Player Count

GameSessionService previously hardcoded MaxCapacity=8 for new sessions,
fragmenting matchmade joiners 8-per-instance regardless of the host's
'Max Player Count in This Subroom' slider. New sessions now resolve the
room's entry subroom (lowest OrderIndex) RoomSceneEntity.MaxPlayers,
clamped 1-80, falling back to 8 for system rooms (dorm GUID roomIds).
2026-06-30 13:35:14 +03:00
Alexa 97f6522497 Add DorkNet.Contracts reference to monolith Server build (#33)
* Route microservices through Traefik behind Cloudflare Tunnel

Add scoped Traefik host labels to the gateway and put cloudflared on the
Traefik network so the tunnel hands off to Traefik instead of forwarding
every host straight to the app. Keep backend services internal-only.

* Add DorkNet.Contracts reference to monolith Server build

DorkNet.Server uses DorkNetRouteOwnership from DorkNet.Contracts but the
project never referenced it and the Dockerfile never copied it, so the
monolith image failed to compile (CS0234). Add the project reference and
copy the project into the build stage.
2026-06-25 12:43:52 +03:00
Alexa e3bbcbe4a4 Route microservices through Traefik behind Cloudflare Tunnel (#32)
Add scoped Traefik host labels to the gateway and put cloudflared on the
Traefik network so the tunnel hands off to Traefik instead of forwarding
every host straight to the app. Keep backend services internal-only.
2026-06-25 12:05:45 +03:00
Alexa 12b1a7f1e2 Restrict gateway to configured public hosts 2026-06-23 00:27:21 +03:00
Alexa 157cc457a9 Harden client mod config loading
Fixes #24 by making the December client mod read the MelonLoader UserData directory directly when available and by accepting config keys case-insensitively.
2026-06-22 12:20:43 +03:00
Alexa fa7486915b Fix notification URLs in HTTP deployments
Fixes #19 by making the December server advertise notification and service URLs with the configured deployment scheme.
2026-06-22 12:09:01 +03:00
Alexa 1794d4e777 Fix Crescendo SQLite upgrade collision
Fixes #21 by making the December legacy room rename safe when SQLite already has a Crescendo row.
2026-06-22 12:05:44 +03:00
Alexa 8426091e91 ci(release): grant contents:write to the release workflow (#16)
The workflow creates a GitHub release and uploads assets, which needs the contents:write token scope. Declaring it per-workflow (matching release-launcher.yml on main) lets the repo keep a read-only default GITHUB_TOKEN.
2026-06-16 11:56:31 +03:00
Alexa fa7dac9ec1 Merge pull request #8 from DorkSquadRR/microservices-scaffold
Enhance DorkNet routing, branding, and microservices documentation
2026-06-16 00:19:59 +03:00
Alexa 67970e5d23 Merge december-2020-12-18 into microservices-scaffold
Resolve modify/delete conflicts in favor of the microservices scaffold:
re-add the Contracts, ServiceDefaults, Gateway, and Identity/Notify/Rooms
service projects that december had removed. Keep december's branding and
route-case updates, and preserve its DorkNet.ApiRouteCoverage tool in the
solution (union of both branches' projects).
2026-06-16 00:16:45 +03:00
Alexa fc5b116263 Merge pull request #3 from DorkSquadRR/december-route-cases
Add December route cases
2026-06-16 00:03:32 +03:00
Alexa e2ec948c74 Document admin page and refresh architecture docs 2026-06-15 23:44:48 +03:00
Alexa c6a4357c6f Update docs for microservices architecture 2026-06-15 23:35:30 +03:00
Alexa 6704505dae Build web assets in service Dockerfile 2026-06-15 23:24:28 +03:00
Alexa 269a600869 Refactor web project asset handling to use DorkNetServerWebAsset for improved publish process 2026-06-15 23:06:38 +03:00
Alexa b3a06ce5e8 Clarify deployment version env keys 2026-06-15 22:48:38 +03:00
Alexa 1b0a9a62b4 Implement version check enhancements and add tests for December 2020 build 2026-06-15 22:45:40 +03:00
Alexa babfd2833f Add network configuration for microservices in Docker Compose 2026-06-15 22:17:14 +03:00
Alexa 78ff4481db Allow custom microservices Postgres connection 2026-06-15 22:06:29 +03:00
Alexa dccefb0158 Route browser paths to web service first 2026-06-15 22:04:04 +03:00
Alexa 49cf91f77d Remove root redirect endpoint from DorkNet service 2026-06-15 21:54:58 +03:00
Alexa 46e2d879cd Add new SVG logo for DorkNet branding 2026-06-15 21:49:35 +03:00
Alexa 26d183af7d Merge pull request #6 from DorkSquadRR/server-branding-december
Match server branding to logo
2026-06-15 21:08:59 +03:00
Alexa 35459fdfdd Match server branding to logo 2026-06-15 21:03:00 +03:00
Alexa 94bad2912b Add endpoint contract tool and GitHub workflow 2026-06-15 20:31:41 +03:00
Alexa 44f53e80b8 Split remaining service routes 2026-06-15 20:21:05 +03:00
Alexa 44282f8f3b Move microservices scaffold back to branch 2026-06-15 16:27:27 +03:00
Alexa bd4ceada65 Use Cloudflare Tunnel for Dokploy routing 2026-06-15 15:31:05 +03:00
Alexa 86be2d004f Add env-driven Dokploy routing 2026-06-15 15:10:16 +03:00
Alexa 3c4c2e660a Merge pull request #4 from DorkSquadRR/microservices-scaffold
Add microservices scaffold
2026-06-15 14:07:04 +03:00
Alexa 08c091c203 Use external S3 storage 2026-06-15 14:04:49 +03:00
Alexa 5f523db485 Add microservices scaffold 2026-06-15 14:01:23 +03:00
107 changed files with 5470 additions and 491 deletions
@@ -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
+3
View File
@@ -11,6 +11,9 @@ on:
tags:
- 'v1-december-*'
permissions:
contents: write # create the GitHub release + upload assets
jobs:
release:
runs-on: windows-latest
+4 -2
View File
@@ -21,7 +21,7 @@ tmp/
## OS
.DS_Store
Thumbs.db
.backups
## Reverse-engineered artifacts (Il2CppDumper / Cpp2IL output, not for commit)
dump.cs
il2cpp.h
@@ -48,6 +48,7 @@ dist$ZIPNAME.zip
## time someone rebuilds. Keep the source committed, regenerate output.
DorkNet.Server/wwwroot/admin/index.html
DorkNet.Server/wwwroot/admin/assets/
DorkNet.Server/wwwroot/admin/brand/
DorkNet.Server/wwwroot/admin/*.map
DorkNet.Server/admin-ui/node_modules/
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.
DorkNet.Server/wwwroot/site/index.html
DorkNet.Server/wwwroot/site/assets/
DorkNet.Server/wwwroot/site/brand/
DorkNet.Server/wwwroot/site/*.map
DorkNet.Server/site/node_modules/
DorkNet.Server/site/dist/
@@ -83,4 +85,4 @@ Recovered/
Recovered(*)/
local-releases/**
local-releases/**
+2
View File
@@ -53,12 +53,14 @@ WORKDIR /src
# DorkNet.Models project ref, so its csproj needs to be present too.
COPY DorkNet.Server/DorkNet.Server.csproj DorkNet.Server/
COPY DorkNet.Models/DorkNet.Models.csproj DorkNet.Models/
COPY DorkNet.Contracts/DorkNet.Contracts.csproj DorkNet.Contracts/
RUN dotnet restore DorkNet.Server/DorkNet.Server.csproj
# 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.
COPY DorkNet.Server/ DorkNet.Server/
COPY DorkNet.Models/ DorkNet.Models/
COPY DorkNet.Contracts/ DorkNet.Contracts/
COPY --from=admin-ui /wwwroot/admin/ DorkNet.Server/wwwroot/admin/
COPY --from=site /wwwroot/site/ DorkNet.Server/wwwroot/site/
+55
View File
@@ -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\""]
+10
View File
@@ -2,6 +2,16 @@
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
The app stores sensitive values in platform secure storage:
+57 -29
View File
@@ -610,17 +610,7 @@ public class Mod : MelonMod
{
try
{
// Compute the UserData path ourselves rather than calling
// 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");
var userData = ResolveUserDataDirectory();
Directory.CreateDirectory(userData);
DiagnosticsLogPath = Path.Combine(userData, "dorknet-diagnostics.log");
var path = Path.Combine(userData, "dorknet-clientmod.json");
@@ -631,21 +621,21 @@ public class Mod : MelonMod
}
var doc = JsonDocument.Parse(File.ReadAllText(path));
var r = doc.RootElement;
if (r.TryGetProperty("ServerHost", out var v)) Cfg.ServerHost = v.GetString() ?? Cfg.ServerHost;
if (r.TryGetProperty("PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
if (r.TryGetProperty("PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
if (r.TryGetProperty("PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
if (r.TryGetProperty("EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
if (r.TryGetProperty("TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
if (r.TryGetProperty("EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
if (r.TryGetProperty("DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
if (r.TryGetProperty("DesktopScreenShareFps", out v)) Cfg.DesktopScreenShareFps = (float)v.GetDouble();
if (r.TryGetProperty("DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean();
if (r.TryGetProperty("DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
if (r.TryGetProperty("DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
if (r.TryGetProperty("DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
if (r.TryGetProperty("QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
if (r.TryGetProperty("QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array)
if (TryGetConfigValue(r, "ServerHost", out var v)) Cfg.ServerHost = v.GetString() ?? Cfg.ServerHost;
if (TryGetConfigValue(r, "PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
if (TryGetConfigValue(r, "PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
if (TryGetConfigValue(r, "PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
if (TryGetConfigValue(r, "EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
if (TryGetConfigValue(r, "TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
if (TryGetConfigValue(r, "EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
if (TryGetConfigValue(r, "DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
if (TryGetConfigValue(r, "DesktopScreenShareFps", out v)) Cfg.DesktopScreenShareFps = (float)v.GetDouble();
if (TryGetConfigValue(r, "DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean();
if (TryGetConfigValue(r, "DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
if (TryGetConfigValue(r, "DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
if (TryGetConfigValue(r, "DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
if (TryGetConfigValue(r, "QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
if (TryGetConfigValue(r, "QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array)
{
var rooms = new List<string>();
foreach (var e in v.EnumerateArray())
@@ -653,8 +643,8 @@ public class Mod : MelonMod
rooms.Add(e.GetString()!);
Cfg.QuestMaxTeamSizeRooms = rooms.ToArray(); // explicit (incl. empty = none)
}
if (r.TryGetProperty("EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
if (r.TryGetProperty("DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
if (TryGetConfigValue(r, "EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
if (TryGetConfigValue(r, "DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
{
var list = new List<string>();
foreach (var e in v.EnumerateArray())
@@ -666,7 +656,7 @@ public class Mod : MelonMod
}
// "InjectAuthValues" key in the template is now ignored —
// 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}");
}
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 ────────────────────────────────────────
private bool TryPatch(string label, Type target, string methodName,
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>
+312
View File
@@ -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);
+19
View File
@@ -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);
+16
View File
@@ -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";
}
+18
View File
@@ -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>
+392
View File
@@ -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 },
},
});
}
+23
View File
@@ -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";
}
+65
View File
@@ -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/me/profileimage")]
[HttpPost("account/me/profileimage")]
[HttpPost("profileimage")]
[HttpPost("account/me/profileimage", Order = 10)]
[HttpPost("profileimage", Order = 10)]
[HttpPut("api/account/v1/profileimage")]
[HttpPut("api/account/me/profileimage")]
[HttpPut("account/me/profileimage")]
[HttpPut("profileimage")]
[HttpPut("account/me/profileimage", Order = 10)]
[HttpPut("profileimage", Order = 10)]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
public async Task<IActionResult> SetProfileImage(
[FromForm(Name = "imageName")] string? imageName,
@@ -15,5 +15,5 @@ public class ServiceUrlsController(DomainConfig domain) : ControllerBase
{
[HttpGet("services")]
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
{
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
@@ -27,7 +27,7 @@ public class NotificationController(DomainConfig domain) : ControllerBase
[HttpGet("/api/notification/hub/v1")]
public IActionResult HubInfo() => Ok(new
{
Url = $"https://{domain.Sub("notify")}/hub/v1",
Url = domain.Url("notify", "/hub/v1"),
Protocol = "signalr",
Version = 1,
});
@@ -41,9 +41,9 @@ public class PageviewController(ILogger<PageviewController> logger) : Controller
/// intentionally return an empty url + a 1-hour freshness — the
/// 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>
[HttpPost("pageview/consume")]
[HttpPost("pageview/consume", Order = 10)]
[HttpPost("/pageview/consume")]
[HttpGet("pageview/consume")]
[HttpGet("pageview/consume", Order = 10)]
[HttpGet("/pageview/consume")]
public IActionResult Consume()
{
@@ -23,7 +23,7 @@ public class AuthAccountController(DorkNetDbContext db) : ControllerBase
/// <c>ExpectPrimitiveResponse&lt;bool&gt;</c> so the body must be
/// the literal JSON <c>true</c> or <c>false</c>.</summary>
[HttpGet("/account/me/haspassword")]
[HttpGet("/api/platformlogin/haspassword")]
[HttpGet("/api/platformlogin/haspassword", Order = 10)]
[Authorize]
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>
[HttpPost("/account/me/changepassword")]
[HttpPost("/account/me/createpassword")]
[HttpPost("/api/platformlogin/password")]
[HttpPut("/api/platformlogin/password")]
[HttpPost("/api/platformlogin/password", Order = 10)]
[HttpPut("/api/platformlogin/password", Order = 10)]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
[Authorize]
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
/// changepassword on next login. Anonymous calls 401.</summary>
[HttpPost("/account/me/resetpassword")]
[HttpPost("/api/platformlogin/resetpassword")]
[HttpPost("/api/platformlogin/resetpassword", Order = 10)]
[Authorize]
[AdminOnly]
public async Task<IActionResult> AdminResetPassword(
@@ -46,6 +46,12 @@ public class VersionCheckController(
private const int VersionStatus_ValidForMenu = 1;
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/v3")]
[HttpGet("/api/versioncheck/v2")]
@@ -66,9 +72,17 @@ public class VersionCheckController(
// }
// Mapping ABSENT → unknown build → UpdateRequired.
var mapSection = config.GetSection("DorkNet:BuildIdToVersionKey");
string? versionKey = !string.IsNullOrWhiteSpace(buildId)
? mapSection[buildId]
: null;
string? versionKey = null;
if (!string.IsNullOrWhiteSpace(buildId))
{
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);
var status = allowed ? VersionStatus_ValidForPlay : VersionStatus_UpdateRequired;
@@ -668,11 +668,14 @@ public class ClubsController(ClubService clubs) : ControllerBase
[HttpPost("/members/bulk")]
public async Task<IActionResult> MembersBulk()
{
var form = Request.HasFormContentType ? Request.Form : null;
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" });
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
.SelectMany(v => (v ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
.Select(s => long.TryParse(s, out var n) ? n : 0)
@@ -20,7 +20,6 @@ namespace DorkNet.Server.Controllers.Match;
[ApiController]
public class MatchPlayerController(
PlayerPresenceService presence,
NotificationService notifications,
RoomService rooms,
PrivateInstanceService privateInstances,
IConfiguration config,
@@ -37,7 +36,7 @@ public class MatchPlayerController(
/// </summary>
[HttpPost("/player/login")]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
public async Task<IActionResult> PlayerLogin([FromForm(Name = "LoginLock")] string? loginLock)
public IActionResult PlayerLogin([FromForm(Name = "LoginLock")] string? loginLock)
{
var playerId = TryGetCurrentPlayerId();
if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock))
@@ -46,17 +45,15 @@ public class MatchPlayerController(
if (replaced)
{
log.LogWarning(
"[player-login] concurrent session for player={PlayerId}; kicking old session(s) via ModerationKick push",
"[player-login] concurrent session marker replaced for player={PlayerId}; stale SignalR connections will be evicted on hub connect",
playerId);
// The new session hasn't opened its SignalR socket yet
// (negotiate fires AFTER /player/login on the watch's
// boot sequence — verified in the live trace), so the
// ModerationKick push hits only the OLDER session's
// existing connections. They'll show the "you've been
// kicked" dialog and gracefully disconnect via the
// watch's LocalModerationKickSelf path.
await notifications.KickStaleSession(playerId,
"Your account was signed in on another device.");
// Do not broadcast ModerationKick to the whole player group
// here. During reconnects the active watch can already have a
// SignalR connection in that group, and the 2020 client crashes
// hard if it receives a malformed/stale moderation payload.
// NotifyHub.OnConnectedAsync snapshots existing connection ids
// before adding the new connection and sends a targeted kick to
// those ids only.
}
}
return Ok(Array.Empty<object>());
@@ -223,9 +220,12 @@ public class MatchPlayerController(
var playerId = TryGetCurrentPlayerId();
if (playerId == 0) return Ok();
var form = Request.Form;
var regions = form["region"].ToArray();
var pings = form["ping"].ToArray();
var regions = Request.HasFormContentType
? Request.Form["region"].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);
for (var i = 0; i < Math.Min(regions.Length, pings.Length); i++)
{
@@ -37,13 +37,13 @@ public class NsController : ControllerBase
// code: locker)" in BootSequence.
[HttpGet("/", Order = -1000)]
public ActionResult<Dictionary<string, string>> Root() =>
Ok(ConfigService.BuildServiceUrlMap(_domain.Apex));
Ok(ConfigService.BuildServiceUrlMap(_domain));
// Some builds fetch /v1 or /v1/services directly
[HttpGet("/v1")]
[HttpGet("/v1/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
[HttpGet("/v1/ping")]
+30 -7
View File
@@ -180,8 +180,13 @@ public static class LegacyUpgrades
// either no-op (Crescendo now exists) or the only writer.
for (var attempt = 1; attempt <= 2; attempt++)
{
var crescendoExists = await db.Rooms.AnyAsync(r => r.Name == "Crescendo", ct);
var bloodMoon = await db.Rooms.FirstOrDefaultAsync(r => r.Name == "BloodMoon", ct);
var renameCandidates = await db.Rooms
.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)
{
@@ -217,15 +222,15 @@ public static class LegacyUpgrades
{
// Both exist — the rename ran on a different row at
// some point, and the original BloodMoon row is now
// a stale duplicate. Park it under CrescendoLegacy
// and hide from browse so /goto/name/... still
// a stale duplicate. Park it under a unique legacy
// name and hide from browse so /goto/name/... still
// resolves Crescendo to the right row.
bloodMoon.Name = "CrescendoLegacy";
bloodMoon.Name = await PickUniqueRoomNameAsync(db, "CrescendoLegacy", bloodMoon.Id, ct);
bloodMoon.HiddenFromBrowse = true;
await db.SaveChangesAsync(ct);
logger.LogInformation(
"[legacy-upgrade] RenameBloodMoonToCrescendo: applied — Rooms.Id={Id} parked as CrescendoLegacy (Crescendo already existed on a different row)",
bloodMoon.Id);
"[legacy-upgrade] RenameBloodMoonToCrescendo: applied — Rooms.Id={Id} parked as {Name} (Crescendo already existed on a different row)",
bloodMoon.Id, bloodMoon.Name);
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}";
}
}
+7 -6
View File
@@ -6,8 +6,9 @@
(generated by `dotnet ef migrations add Initial` against the
current entity model — captures all 67 `DbSet<>` tables).
- **`LegacyUpgrades.cs`** holds idempotent data transforms that can't
be derived from the entity model. Runs once per server boot, after
EF migrations complete and before `RoomService.SeedAsync()`.
be derived from the entity model. Runs during host boot for the
standalone server and for service-slice containers, after schema
bootstrap completes and before `RoomService.SeedAsync()`.
## What changed in the consolidation
@@ -29,7 +30,7 @@ Both were collapsed in 2026-05-25:
no per-feature patches needed.
- The PhotonRegion coerce (the only data transform in the block)
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
draining the legacy `RoomDataBlobs.Bytes` column into S3, which is
no longer needed.
@@ -37,8 +38,8 @@ Both were collapsed in 2026-05-25:
## Production cutover (one-time SQL)
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
`CREATE TABLE` over existing data.
Initial is already applied here" so the next standalone or service-slice
boot doesn't try to `CREATE TABLE` over existing data.
### 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
there's *nothing to do* — `EnsureCreated()` reads the entity model,
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
before the cutover deploy:
+7
View File
@@ -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`.
> 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
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:
+10
View File
@@ -73,6 +73,16 @@
<ItemGroup>
<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>
<!-- Vite admin SPA build. Source at admin-ui/, output goes to
+2
View File
@@ -15,3 +15,5 @@ var app = builder.Build();
await app.RunDatabaseBootstrapAsync();
app.UseDorkNetPipeline();
app.Run();
public partial class Program;
+6 -4
View File
@@ -25,9 +25,9 @@ public class ConfigService(IConfiguration config, DomainConfig domain)
return new RecRoomConfig
{
MessageOfTheDay = config["Server:MOTD"] ?? "Welcome to the private server!",
CdnBaseUri = $"https://{domain.Sub("cdn")}",
CdnBaseUri = domain.Url("cdn"),
PhotonConfig = photon,
ServiceUrls = BuildServiceUrlMap(apex),
ServiceUrls = BuildServiceUrlMap(domain),
ConfigTable = new Dictionary<string, string>
{
// 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
/// source of truth.</summary>
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);
foreach (var (service, sub) in ServiceSubdomains)
{
var host = string.IsNullOrEmpty(sub) ? apex : $"{sub}.{apex}";
map[service] = $"https://{host}";
map[service] = domain.Url(sub);
}
return map;
}
+21 -1
View File
@@ -11,8 +11,28 @@ namespace DorkNet.Server.Services;
public sealed class DomainConfig
{
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 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)
=> 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";
}
}
+28 -1
View File
@@ -64,7 +64,12 @@ public class GameSessionService(DorkNetDbContext db)
// in, so we save once with a placeholder, then patch and save
// again. Two SaveChangesAsync calls is cheap at our QPS.
PhotonRoomName = "pending",
MaxCapacity = 8,
// Track the room's "Max Player Count in This Subroom" slider
// instead of a hardcoded 8, so matchmade joiners fill the room to
// the host's chosen limit rather than fragmenting into a new
// instance every 8 players. (Existing sessions keep whatever cap
// they were created with.)
MaxCapacity = await ResolveRoomMaxCapacityAsync(effectiveRoom),
PlayerCount = 1,
CreatedAt = DateTime.UtcNow,
};
@@ -75,6 +80,28 @@ public class GameSessionService(DorkNetDbContext db)
return ToDto(session);
}
// Default session cap when we can't resolve a room-specific value (system
// rooms like the dorm use a GUID roomId that doesn't map to a RoomEntity).
private const int DefaultMaxCapacity = 8;
/// <summary>Resolve a new session's capacity from the room's entry
/// subroom (lowest <c>OrderIndex</c>) <see cref="RoomSceneEntity.MaxPlayers"/>
/// — the value the in-game "Max Player Count in This Subroom" slider
/// writes. Matchmade sessions land in the entry subroom, so its cap is the
/// right ceiling. GameSession roomIds are the room's numeric Id as a
/// string for user rooms; non-numeric (dorm GUID, etc.) → default. Clamped
/// to a sane Photon ceiling.</summary>
private async Task<int> ResolveRoomMaxCapacityAsync(string roomId)
{
if (!long.TryParse(roomId, out var roomLongId)) return DefaultMaxCapacity;
var max = await db.RoomScenes
.Where(s => s.RoomId == roomLongId)
.OrderBy(s => s.OrderIndex)
.Select(s => (int?)s.MaxPlayers)
.FirstOrDefaultAsync();
return max is int v && v > 0 ? Math.Clamp(v, 1, 80) : DefaultMaxCapacity;
}
public async Task<GameSession?> GetByIdAsync(long id)
{
var s = await db.GameSessions.AsNoTracking()
@@ -70,7 +70,7 @@ public static class MiddlewarePipelineExtensions
if (ctx.Request.Host.Host.Equals(domainCfg.Sub("api"), StringComparison.OrdinalIgnoreCase) &&
(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;
}
await next();
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using DorkNet.Contracts;
using DorkNet.Server.Compat;
using DorkNet.Server.Data;
using DorkNet.Server.Services;
@@ -106,14 +107,19 @@ public static class ServiceCollectionExtensions
builder.Configuration["Domain:Apex"]
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
?? "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 =>
{
opt.AllowedHosts = new[] { apex, $"*.{apex}", "localhost", "127.0.0.1" };
opt.AllowedHosts = allowedHosts;
opt.AllowEmptyHosts = true;
opt.IncludeFailureMessage = true;
});
Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{apex}, *.{apex}, localhost]");
Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{string.Join(", ", allowedHosts)}]");
}
// ── Services ──────────────────────────────────────────────────────────────
+2
View File
@@ -4,7 +4,9 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="color-scheme" content="dark light" />
<meta name="theme-color" content="#0b95c7" />
<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.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" />
@@ -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,
Megaphone, MessageSquare, Settings, X,
} from './Icons';
import { BrandMark } from './BrandMark';
// Consolidated navigation. Per-player moderation (bans, grants, gifts,
// 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="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 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>
</div>
+4 -3
View File
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { api, ApiError } from '../lib/api';
import { notifyAuthChange, setSession } from '../lib/auth';
import { BrandMark } from '../components/BrandMark';
interface LoginResponse {
access_token: string;
@@ -51,12 +52,12 @@ export function Login() {
};
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="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">
<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>
</div>
</div>
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+11 -11
View File
@@ -20,17 +20,17 @@ export default {
950: '#0a0b0e',
},
brand: {
DEFAULT: '#7c5cff',
50: '#f1edff',
100: '#e1d8ff',
200: '#c5b6ff',
300: '#a48dff',
400: '#8d72ff',
500: '#7c5cff',
600: '#6948e3',
700: '#5236b8',
800: '#3e2989',
900: '#2a1c5f',
DEFAULT: '#0b95c7',
50: '#ecfbff',
100: '#c8f3ff',
200: '#8bedff',
300: '#51e5ff',
400: '#22cfee',
500: '#0b95c7',
600: '#087aa8',
700: '#096083',
800: '#0a465f',
900: '#082f40',
},
success: '#3ecf8e',
warn: '#f4b740',
+2
View File
@@ -4,8 +4,10 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="color-scheme" content="dark light" />
<meta name="theme-color" content="#0b95c7" />
<title>DorkNet</title>
<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.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" />
@@ -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 { useEffect, useRef, useState } from 'react';
import { BrandMark } from './BrandMark';
const NAV = [
{ 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">
<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">
<span className="inline-block size-6 rounded-md bg-gradient-to-br from-brand-400 to-brand-700 shadow-inner" />
<span className="font-semibold text-ink-50 tracking-tight">DorkNet</span>
<BrandMark className="size-7" />
<span className="font-semibold text-ink-50">DorkNet</span>
</Link>
<nav className="hidden md:flex items-center gap-1 ml-2">
+2 -4
View File
@@ -16,11 +16,9 @@ export function Home() {
return (
<div className="space-y-10">
<section className="card !p-6 sm:!p-10 relative overflow-hidden">
<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" />
<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="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.
</h1>
<p className="mt-3 text-ink-300 text-base sm:text-lg">
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+11 -11
View File
@@ -20,17 +20,17 @@ export default {
950: '#0a0b0e',
},
brand: {
DEFAULT: '#7c5cff',
50: '#f1edff',
100: '#e1d8ff',
200: '#c5b6ff',
300: '#a48dff',
400: '#8d72ff',
500: '#7c5cff',
600: '#6948e3',
700: '#5236b8',
800: '#3e2989',
900: '#2a1c5f',
DEFAULT: '#0b95c7',
50: '#ecfbff',
100: '#c8f3ff',
200: '#8bedff',
300: '#51e5ff',
400: '#22cfee',
500: '#0b95c7',
600: '#087aa8',
700: '#096083',
800: '#0a465f',
900: '#082f40',
},
success: '#3ecf8e',
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

+10 -2
View File
@@ -3,12 +3,17 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#0b95c7">
<title>DorkNet Admin</title>
<link rel="icon" type="image/svg+xml" href="brand/dorknet.svg">
<link rel="stylesheet" href="style.css">
</head>
<body>
<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>
<form id="loginForm">
<label>Username<input id="loginUsername" autocomplete="username" required></label>
@@ -20,7 +25,10 @@
<div id="app" hidden>
<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">
Signed in as <span id="meName"></span>
<button id="logoutBtn" class="link">log out</button>
+33 -12
View File
@@ -6,7 +6,7 @@
color-scheme: dark;
--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-solid: #161a23;
--panel-2: #1d2230;
@@ -17,10 +17,10 @@
--text-dim: #b3b9c9;
--muted: #7a8195;
--accent: #7aa9ff;
--accent-2: #b48cff;
--accent-grad: linear-gradient(135deg, #7aa9ff 0%, #b48cff 100%);
--accent-hover: #93b9ff;
--accent: #33ebff;
--accent-2: #0b95c7;
--accent-grad: linear-gradient(135deg, #33ebff 0%, #0b95c7 100%);
--accent-hover: #c8f3ff;
--good: #4ad28b;
--good-bg: rgba(74, 210, 139, 0.12);
@@ -28,7 +28,7 @@
--warn-bg: rgba(255, 206, 91, 0.12);
--danger: #ff6f7d;
--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-lg: 14px;
@@ -107,10 +107,10 @@ button[type="submit"], button.primary {
border-color: transparent;
color: #0a0c10;
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 {
box-shadow: 0 6px 20px rgba(122, 169, 255, 0.4);
box-shadow: 0 6px 20px rgba(51, 235, 255, 0.28);
}
button.danger {
@@ -158,7 +158,7 @@ input:hover, textarea:hover, select:hover { border-color: #3a425a; }
input:focus, textarea:focus, select:focus {
outline: none;
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"] {
width: 16px;
@@ -215,6 +215,27 @@ label > select {
#login p { margin: 0 0 22px; }
#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 {
from { opacity: 0; transform: translateY(12px); }
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 {
background: var(--info-bg);
color: var(--accent);
border-color: rgba(122, 169, 255, 0.25);
border-color: rgba(51, 235, 255, 0.24);
}
.tab-panel {
@@ -382,7 +403,7 @@ nav .tab.active {
}
.table tr:last-child td { border-bottom: none; }
.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 {
display: flex;
@@ -419,7 +440,7 @@ nav .tab.active {
.badge.no-dot::before { display: none; }
.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.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-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 -1
View File
@@ -3,13 +3,15 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#0b95c7">
<title>DorkNet — Feed</title>
<link rel="icon" type="image/svg+xml" href="brand/dorknet.svg">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="site-header">
<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>
</a>
<nav class="site-nav">
+19 -13
View File
@@ -5,7 +5,7 @@
:root {
color-scheme: dark;
--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-2: #161922;
--panel-3: #1f2330;
@@ -14,9 +14,9 @@
--text: #f0f2f8;
--text-dim: #b0b6c8;
--muted: #757b8e;
--accent: #ff6ec7;
--accent-2: #7aa9ff;
--accent-grad: linear-gradient(135deg, #ff6ec7 0%, #b48cff 50%, #7aa9ff 100%);
--accent: #33ebff;
--accent-2: #0b95c7;
--accent-grad: linear-gradient(135deg, #33ebff 0%, #0b95c7 100%);
--good: #4ad28b;
--danger: #ff6f7d;
--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:hover { color: #a3c4ff; }
a:hover { color: #c8f3ff; }
button {
background: var(--panel-2);
@@ -61,15 +61,15 @@ button.primary {
border-color: transparent;
color: #07080b;
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 {
background: transparent;
border-color: var(--border);
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 ──────────────────────────────────────────────────── */
@@ -97,13 +97,19 @@ button.ghost:hover { color: var(--text); border-color: var(--accent); background
}
.logo:hover { color: var(--text); }
.logo-mark {
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--accent-grad);
width: 28px;
height: 28px;
border-radius: 8px;
background: #07080b;
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 {
display: flex;
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>
+37
View File
@@ -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>
+37
View File
@@ -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>
+35
View File
@@ -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>
+37
View File
@@ -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>
+34
View File
@@ -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();
+8
View File
@@ -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>
+37
View File
@@ -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>
+37
View File
@@ -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();
+8
View File
@@ -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>
+37
View File
@@ -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();
+8
View File
@@ -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>
+36
View File
@@ -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();
+8
View File
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+198 -31
View File
@@ -7,13 +7,37 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Server", "DorkNet.S
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DorkNet.Models", "DorkNet.Models\DorkNet.Models.csproj", "{8A1F0C3A-5763-4176-B33E-F4EA89BAF999}"
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}"
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
Global
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|x86.ActiveCfg = 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.Build.0 = 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|x86.ActiveCfg = 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
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Debug|x64.ActiveCfg = Debug|Any CPU
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Debug|x64.Build.0 = Debug|Any CPU
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Debug|x86.ActiveCfg = Debug|Any CPU
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Debug|x86.Build.0 = Debug|Any CPU
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|Any CPU.Build.0 = Release|Any CPU
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|x64.ActiveCfg = Release|Any CPU
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|x64.Build.0 = Release|Any CPU
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|x86.ActiveCfg = Release|Any CPU
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED}.Release|x86.Build.0 = Release|Any CPU
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|x64.ActiveCfg = Debug|Any CPU
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|x64.Build.0 = Debug|Any CPU
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|x86.ActiveCfg = Debug|Any CPU
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Debug|x86.Build.0 = Debug|Any CPU
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Release|Any CPU.Build.0 = Release|Any CPU
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Release|x64.ActiveCfg = Release|Any CPU
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Release|x64.Build.0 = Release|Any CPU
{7D9E995E-5333-43B2-848B-3522C6C3D893}.Release|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{70C60098-1B11-4C13-943B-A8A056429EAE} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
{AB8D33A2-8F4C-4E10-BD25-51343D20F7ED} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
{6BC447E7-3158-4FEC-81D4-506452157810} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
EndGlobalSection
EndGlobal
+50 -5
View File
@@ -23,6 +23,7 @@ main also auto-detects your install.
| **Version key** (sent as `X-DorkNet-Version` header) | `december_2020_12_18` |
| **Version plugin** | `DorkNet.Server/Versions/Late2020/Late2020VersionPlugin.cs` |
| **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 |
Subsystems present on this branch that the March branch doesn't have:
@@ -58,7 +59,7 @@ cd DorkNet
| 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:VoiceAppId` | `Photon__VoiceAppId` | no | only if voice chat |
| `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:Redis` | `ConnectionStrings__Redis` | no | enables SignalR backplane |
| `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
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`
returns 200.
For a full Docker / VPS deploy, see
[main's Advanced setup guide](../../blob/main/docs/advanced-setup.md).
For the production microservices stack, Docker, and Dokploy deploy
notes, see
[docs/deploy.md](docs/deploy.md).
---
@@ -194,15 +200,54 @@ Default `0` (off).
| 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.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 |
| `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
model (project layout, request lifecycle, watch-mirror controller
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
+235
View File
@@ -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
+192
View File
@@ -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:
+136
View File
@@ -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 |
+94 -17
View File
@@ -14,15 +14,39 @@ files in this folder.
```
DorkNet.sln
├── DorkNet.Server ASP.NET Core 9 backend — the bulk of the code
├── DorkNet.Models Shared DTO + serialization types
└── DorkNet.ClientMod MelonLoader 6 IL2CPP mod — the client patcher
├── DorkNet.Server Shared ASP.NET Core backend + monolith fallback
├── DorkNet.Models Shared DTO + serialization types
├── 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 |
|---|---|---|
| `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.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 |
`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/
├── site/ Public website Vite SPA source — builds to wwwroot/site/
├── 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
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`.
- **Postgres** — production. Connection string from `ConnectionStrings:Default`.
Schema lives in `Data/Entities/*` and `Data/DorkNetDbContext.cs`. EF
migrations under `Data/Migrations/` apply via `Database.Migrate()` on
boot. 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
Schema lives in `Data/Entities/*` and `Data/DorkNetDbContext.cs`.
SQLite dev DBs apply EF migrations under `Data/Migrations/` via
`Database.Migrate()`. The production Postgres path still uses
`EnsureCreated()` plus idempotent bootstrap work under the same
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
discipline: one release = one migration; everything else is
`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
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.
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
shippable. Comments give a future contributor a breadcrumb to
re-derive the wire shape if the decompile changes.
- **`Program.cs` is long.** Most of it is one-time schema patching
(`RunPatchAsync` calls for the Postgres `EnsureCreated` path) that's
pending consolidation per [`Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md).
Once that lands, the remaining ~500 lines of middleware + host
setup will be split into `Startup/*` extensions.
- **Microservices use shared server code during extraction.** The
dedicated service hosts run the proven controller/service stack behind
route guards instead of copying controllers into new projects. This
keeps the client contract stable while route families are peeled away
from the monolith fallback.
- **Two-provider EF.** SQLite and Postgres each have their own
EnsureCreated/Migrate dance because some Postgres-only column types
drift the model snapshot. The split is annoying but lets one

Some files were not shown because too many files have changed in this diff Show More