Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 399f49fa8a | |||
| c192d9c051 | |||
| c80b5c56eb | |||
| efad7e10d0 | |||
| 88740b1102 | |||
| af1ec31fab | |||
| 5f2fd2875f | |||
| 2e7b39c2cd | |||
| 3e4d618aa7 | |||
| 72c076535c | |||
| 97f6522497 | |||
| e3bbcbe4a4 | |||
| 12b1a7f1e2 | |||
| 157cc457a9 | |||
| fa7486915b | |||
| 1794d4e777 | |||
| 02365f5baf | |||
| 8426091e91 | |||
| fa7dac9ec1 | |||
| 67970e5d23 | |||
| fc5b116263 | |||
| e2ec948c74 | |||
| c6a4357c6f | |||
| 6704505dae | |||
| 269a600869 | |||
| b3a06ce5e8 | |||
| 1b0a9a62b4 | |||
| babfd2833f | |||
| 78ff4481db | |||
| dccefb0158 | |||
| 49cf91f77d | |||
| 46e2d879cd | |||
| 26d183af7d | |||
| 35459fdfdd | |||
| 94bad2912b | |||
| 44f53e80b8 | |||
| 44282f8f3b | |||
| bd4ceada65 | |||
| 86be2d004f | |||
| 3c4c2e660a | |||
| 08c091c203 | |||
| 5f523db485 | |||
| 3755593dbb |
@@ -0,0 +1,78 @@
|
|||||||
|
# Disabled template. GitHub ignores files outside .github/workflows/.
|
||||||
|
# Move this file to .github/workflows/endpoint-contract.yml when it should run.
|
||||||
|
name: Endpoint Contract
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
endpoint-contract:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
ASPNETCORE_ENVIRONMENT: Development
|
||||||
|
ASPNETCORE_URLS: http://127.0.0.1:18080
|
||||||
|
Database__Provider: sqlite
|
||||||
|
Database__SqlitePath: ${{ runner.temp }}/dorknet-endpoint-contract.db
|
||||||
|
DORKNET_JWT_SECRET: endpoint-contract-secret-000000000000000000000000000000000000000000
|
||||||
|
S3__Endpoint: ""
|
||||||
|
S3__AccessKey: ""
|
||||||
|
S3__SecretKey: ""
|
||||||
|
PROTOBUF_PROTOC: /usr/bin/protoc
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
|
- name: Install protobuf compiler
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y protobuf-compiler
|
||||||
|
|
||||||
|
- name: Build server
|
||||||
|
run: dotnet build DorkNet.Server/DorkNet.Server.csproj
|
||||||
|
|
||||||
|
- name: Build endpoint contract runner
|
||||||
|
run: dotnet build tools/DorkNet.EndpointContract/DorkNet.EndpointContract.csproj --no-restore
|
||||||
|
|
||||||
|
- name: Start server
|
||||||
|
run: |
|
||||||
|
dotnet DorkNet.Server/bin/Debug/net9.0/DorkNet.Server.dll > "$RUNNER_TEMP/dorknet-server.log" 2>&1 &
|
||||||
|
echo "$!" > "$RUNNER_TEMP/dorknet-server.pid"
|
||||||
|
|
||||||
|
- name: Wait for server readiness
|
||||||
|
run: |
|
||||||
|
for attempt in {1..60}; do
|
||||||
|
if curl --fail --silent http://127.0.0.1:18080/healthz; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
cat "$RUNNER_TEMP/dorknet-server.log"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Run endpoint contract
|
||||||
|
run: |
|
||||||
|
dotnet run --project tools/DorkNet.EndpointContract/DorkNet.EndpointContract.csproj --no-build -- \
|
||||||
|
--server-assembly DorkNet.Server/bin/Debug/net9.0/DorkNet.Server.dll \
|
||||||
|
--base-url http://127.0.0.1:18080 \
|
||||||
|
--report "$RUNNER_TEMP/endpoint-contract-report.json"
|
||||||
|
|
||||||
|
- name: Upload endpoint contract artifacts
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: endpoint-contract
|
||||||
|
path: |
|
||||||
|
${{ runner.temp }}/endpoint-contract-report.json
|
||||||
|
${{ runner.temp }}/dorknet-server.log
|
||||||
|
|
||||||
|
- name: Stop server
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ -f "$RUNNER_TEMP/dorknet-server.pid" ]; then
|
||||||
|
kill "$(cat "$RUNNER_TEMP/dorknet-server.pid")" || true
|
||||||
|
fi
|
||||||
@@ -11,6 +11,9 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- 'v1-december-*'
|
- 'v1-december-*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # create the GitHub release + upload assets
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ tmp/
|
|||||||
## OS
|
## OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
.backups
|
||||||
## Reverse-engineered artifacts (Il2CppDumper / Cpp2IL output, not for commit)
|
## Reverse-engineered artifacts (Il2CppDumper / Cpp2IL output, not for commit)
|
||||||
dump.cs
|
dump.cs
|
||||||
il2cpp.h
|
il2cpp.h
|
||||||
@@ -48,6 +48,7 @@ dist$ZIPNAME.zip
|
|||||||
## time someone rebuilds. Keep the source committed, regenerate output.
|
## time someone rebuilds. Keep the source committed, regenerate output.
|
||||||
DorkNet.Server/wwwroot/admin/index.html
|
DorkNet.Server/wwwroot/admin/index.html
|
||||||
DorkNet.Server/wwwroot/admin/assets/
|
DorkNet.Server/wwwroot/admin/assets/
|
||||||
|
DorkNet.Server/wwwroot/admin/brand/
|
||||||
DorkNet.Server/wwwroot/admin/*.map
|
DorkNet.Server/wwwroot/admin/*.map
|
||||||
DorkNet.Server/admin-ui/node_modules/
|
DorkNet.Server/admin-ui/node_modules/
|
||||||
DorkNet.Server/admin-ui/dist/
|
DorkNet.Server/admin-ui/dist/
|
||||||
@@ -58,6 +59,7 @@ DorkNet.Server/admin-ui/tsconfig.tsbuildinfo
|
|||||||
## Same reasoning as admin-ui above: hashed output, regenerate per build.
|
## Same reasoning as admin-ui above: hashed output, regenerate per build.
|
||||||
DorkNet.Server/wwwroot/site/index.html
|
DorkNet.Server/wwwroot/site/index.html
|
||||||
DorkNet.Server/wwwroot/site/assets/
|
DorkNet.Server/wwwroot/site/assets/
|
||||||
|
DorkNet.Server/wwwroot/site/brand/
|
||||||
DorkNet.Server/wwwroot/site/*.map
|
DorkNet.Server/wwwroot/site/*.map
|
||||||
DorkNet.Server/site/node_modules/
|
DorkNet.Server/site/node_modules/
|
||||||
DorkNet.Server/site/dist/
|
DorkNet.Server/site/dist/
|
||||||
@@ -83,4 +85,4 @@ Recovered/
|
|||||||
Recovered(*)/
|
Recovered(*)/
|
||||||
|
|
||||||
|
|
||||||
local-releases/**
|
local-releases/**
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# DorkNet.Server container image — Coolify deploy target.
|
# DorkNet.Server container image — Coolify deploy target.
|
||||||
# ────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────
|
||||||
# Multi-stage: SDK image builds + publishes, runtime image is the
|
# Multi-stage: SDK image builds + publishes, runtime image is the
|
||||||
# slimmer aspnet:9.0 with only the published output.
|
# slimmer aspnet:10.0 with only the published output.
|
||||||
#
|
#
|
||||||
# Build context: repo root (so Tools/ and DorkNet.Models/ are visible
|
# Build context: repo root (so Tools/ and DorkNet.Models/ are visible
|
||||||
# to ProjectReference + dotnet restore). The .dockerignore at the
|
# to ProjectReference + dotnet restore). The .dockerignore at the
|
||||||
# repo root keeps the bin/, obj/, and other dev junk out of the
|
# repo root keeps the bin/, obj/, and other dev junk out of the
|
||||||
# context.
|
# context.
|
||||||
|
|
||||||
ARG DOTNET_VERSION=9.0
|
ARG DOTNET_VERSION=10.0
|
||||||
ARG NODE_VERSION=20
|
ARG NODE_VERSION=20
|
||||||
|
|
||||||
# ── admin SPA build stage ──────────────────────────────────────────
|
# ── admin SPA build stage ──────────────────────────────────────────
|
||||||
@@ -53,12 +53,14 @@ WORKDIR /src
|
|||||||
# DorkNet.Models project ref, so its csproj needs to be present too.
|
# DorkNet.Models project ref, so its csproj needs to be present too.
|
||||||
COPY DorkNet.Server/DorkNet.Server.csproj DorkNet.Server/
|
COPY DorkNet.Server/DorkNet.Server.csproj DorkNet.Server/
|
||||||
COPY DorkNet.Models/DorkNet.Models.csproj DorkNet.Models/
|
COPY DorkNet.Models/DorkNet.Models.csproj DorkNet.Models/
|
||||||
|
COPY DorkNet.Contracts/DorkNet.Contracts.csproj DorkNet.Contracts/
|
||||||
RUN dotnet restore DorkNet.Server/DorkNet.Server.csproj
|
RUN dotnet restore DorkNet.Server/DorkNet.Server.csproj
|
||||||
|
|
||||||
# Now copy the rest of the .NET source, then drop the prebuilt admin
|
# Now copy the rest of the .NET source, then drop the prebuilt admin
|
||||||
# SPA into wwwroot/admin so the publish step ships it as static content.
|
# SPA into wwwroot/admin so the publish step ships it as static content.
|
||||||
COPY DorkNet.Server/ DorkNet.Server/
|
COPY DorkNet.Server/ DorkNet.Server/
|
||||||
COPY DorkNet.Models/ DorkNet.Models/
|
COPY DorkNet.Models/ DorkNet.Models/
|
||||||
|
COPY DorkNet.Contracts/ DorkNet.Contracts/
|
||||||
COPY --from=admin-ui /wwwroot/admin/ DorkNet.Server/wwwroot/admin/
|
COPY --from=admin-ui /wwwroot/admin/ DorkNet.Server/wwwroot/admin/
|
||||||
COPY --from=site /wwwroot/site/ DorkNet.Server/wwwroot/site/
|
COPY --from=site /wwwroot/site/ DorkNet.Server/wwwroot/site/
|
||||||
|
|
||||||
@@ -76,13 +78,13 @@ WORKDIR /app
|
|||||||
|
|
||||||
# curl + wget for healthcheck probes — Coolify v4 defaults to wget for
|
# curl + wget for healthcheck probes — Coolify v4 defaults to wget for
|
||||||
# its own probe regardless of what the image's HEALTHCHECK uses, so
|
# its own probe regardless of what the image's HEALTHCHECK uses, so
|
||||||
# both have to be present. The aspnet:9.0 base image strips both by
|
# both have to be present. The aspnet:10.0 base image strips both by
|
||||||
# default to keep the surface area small.
|
# default to keep the surface area small.
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends curl wget \
|
&& apt-get install -y --no-install-recommends curl wget \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Run as a non-root user — aspnet:9.0 ships with the `app` UID 1654.
|
# Run as a non-root user — aspnet:10.0 ships with the `app` UID 1654.
|
||||||
# Coolify mounts persistent volumes with write permission for this UID.
|
# Coolify mounts persistent volumes with write permission for this UID.
|
||||||
USER app
|
USER app
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
ARG DOTNET_VERSION=10.0
|
||||||
|
ARG NODE_VERSION=20
|
||||||
|
|
||||||
|
FROM node:${NODE_VERSION}-alpine AS admin-ui
|
||||||
|
WORKDIR /ui
|
||||||
|
COPY DorkNet.Server/admin-ui/package.json DorkNet.Server/admin-ui/package-lock.json* ./
|
||||||
|
RUN npm install --no-fund --no-audit --loglevel=error
|
||||||
|
COPY DorkNet.Server/admin-ui/ ./
|
||||||
|
RUN mkdir -p /wwwroot/admin \
|
||||||
|
&& npm run build
|
||||||
|
|
||||||
|
FROM node:${NODE_VERSION}-alpine AS site
|
||||||
|
WORKDIR /ui
|
||||||
|
COPY DorkNet.Server/site/package.json DorkNet.Server/site/package-lock.json* ./
|
||||||
|
RUN npm install --no-fund --no-audit --loglevel=error
|
||||||
|
COPY DorkNet.Server/site/ ./
|
||||||
|
RUN mkdir -p /wwwroot/site \
|
||||||
|
&& npm run build
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS build
|
||||||
|
ARG PROJECT_PATH
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
COPY --from=admin-ui /wwwroot/admin/ DorkNet.Server/wwwroot/admin/
|
||||||
|
COPY --from=site /wwwroot/site/ DorkNet.Server/wwwroot/site/
|
||||||
|
RUN dotnet publish "${PROJECT_PATH}" -c Release \
|
||||||
|
-p:SkipAdminUIBuild=true \
|
||||||
|
-p:DorkNetServiceContainerPublish=true \
|
||||||
|
-o /app/publish
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION} AS runtime
|
||||||
|
ARG APP_DLL
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
COPY --from=build --chown=app:app /app/publish .
|
||||||
|
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production \
|
||||||
|
ASPNETCORE_URLS=http://0.0.0.0:8080 \
|
||||||
|
DOTNET_RUNNING_IN_CONTAINER=true \
|
||||||
|
DOTNET_PRINT_TELEMETRY_MESSAGE=false \
|
||||||
|
APP_DLL=${APP_DLL}
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
|
||||||
|
CMD curl --fail --silent http://localhost:8080/healthz || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["sh", "-c", "dotnet \"$APP_DLL\""]
|
||||||
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
Native Android/iOS admin app for DorkNet, built with .NET MAUI.
|
Native Android/iOS admin app for DorkNet, built with .NET MAUI.
|
||||||
|
|
||||||
|
In the microservices deployment, point the app at the public admin host
|
||||||
|
for the stack, usually `https://admin.<domain>`. Cloudflare and
|
||||||
|
`DorkNet.Gateway` keep the public URL stable. Requests made through the
|
||||||
|
admin host are handled by the `web` service's same-origin admin route;
|
||||||
|
the `/api/admin/*` path family is also exposed by the moderation slice
|
||||||
|
for non-admin-host routing.
|
||||||
|
|
||||||
|
The browser admin page, route map, and troubleshooting flow are
|
||||||
|
documented in [`../docs/admin.md`](../docs/admin.md).
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
The app stores sensitive values in platform secure storage:
|
The app stores sensitive values in platform secure storage:
|
||||||
|
|||||||
@@ -610,17 +610,7 @@ public class Mod : MelonMod
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Compute the UserData path ourselves rather than calling
|
var userData = ResolveUserDataDirectory();
|
||||||
// MelonLoader.MelonUtils.UserDataDirectory (which is renamed
|
|
||||||
// to MelonEnvironment.UserDataDirectory in newer 0.6.x builds
|
|
||||||
// and shifts again in 0.7.x). Anchoring off the mod DLL's
|
|
||||||
// location keeps this stable across versions: the DLL lives
|
|
||||||
// in <game>/Mods/, so two dirname()'s reach the game root,
|
|
||||||
// then MelonLoader/UserData/ is right next to it.
|
|
||||||
var modDll = Assembly.GetExecutingAssembly().Location;
|
|
||||||
var modsDir = Path.GetDirectoryName(modDll) ?? string.Empty;
|
|
||||||
var gameDir = Path.GetDirectoryName(modsDir) ?? string.Empty;
|
|
||||||
var userData = Path.Combine(gameDir, "MelonLoader", "UserData");
|
|
||||||
Directory.CreateDirectory(userData);
|
Directory.CreateDirectory(userData);
|
||||||
DiagnosticsLogPath = Path.Combine(userData, "dorknet-diagnostics.log");
|
DiagnosticsLogPath = Path.Combine(userData, "dorknet-diagnostics.log");
|
||||||
var path = Path.Combine(userData, "dorknet-clientmod.json");
|
var path = Path.Combine(userData, "dorknet-clientmod.json");
|
||||||
@@ -631,21 +621,21 @@ public class Mod : MelonMod
|
|||||||
}
|
}
|
||||||
var doc = JsonDocument.Parse(File.ReadAllText(path));
|
var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
var r = doc.RootElement;
|
var r = doc.RootElement;
|
||||||
if (r.TryGetProperty("ServerHost", out var v)) Cfg.ServerHost = v.GetString() ?? Cfg.ServerHost;
|
if (TryGetConfigValue(r, "ServerHost", out var v)) Cfg.ServerHost = v.GetString() ?? Cfg.ServerHost;
|
||||||
if (r.TryGetProperty("PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
|
if (TryGetConfigValue(r, "PhotonAppId", out v)) Cfg.PhotonAppId = v.GetString() ?? Cfg.PhotonAppId;
|
||||||
if (r.TryGetProperty("PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
|
if (TryGetConfigValue(r, "PhotonVoiceAppId", out v)) Cfg.PhotonVoiceAppId = v.GetString() ?? Cfg.PhotonVoiceAppId;
|
||||||
if (r.TryGetProperty("PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
|
if (TryGetConfigValue(r, "PhotonCloudRegion", out v)) Cfg.PhotonCloudRegion = v.GetString() ?? Cfg.PhotonCloudRegion;
|
||||||
if (r.TryGetProperty("EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
|
if (TryGetConfigValue(r, "EnableTlsTrustBypass", out v)) Cfg.EnableTlsTrustBypass = v.GetBoolean();
|
||||||
if (r.TryGetProperty("TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
|
if (TryGetConfigValue(r, "TraceRegistrationDialog", out v)) Cfg.TraceRegistrationDialog = v.GetBoolean();
|
||||||
if (r.TryGetProperty("EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
if (TryGetConfigValue(r, "EnableDebugConsole", out v)) Cfg.EnableDebugConsole = v.GetBoolean();
|
||||||
if (r.TryGetProperty("DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
|
if (TryGetConfigValue(r, "DebugConsoleToggleKey", out v)) Cfg.DebugConsoleToggleKey = v.GetString() ?? Cfg.DebugConsoleToggleKey;
|
||||||
if (r.TryGetProperty("DesktopScreenShareFps", out v)) Cfg.DesktopScreenShareFps = (float)v.GetDouble();
|
if (TryGetConfigValue(r, "DesktopScreenShareFps", out v)) Cfg.DesktopScreenShareFps = (float)v.GetDouble();
|
||||||
if (r.TryGetProperty("DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean();
|
if (TryGetConfigValue(r, "DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean();
|
||||||
if (r.TryGetProperty("DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
|
if (TryGetConfigValue(r, "DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32();
|
||||||
if (r.TryGetProperty("DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
|
if (TryGetConfigValue(r, "DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32();
|
||||||
if (r.TryGetProperty("DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
if (TryGetConfigValue(r, "DiagnoseDevMenu", out v)) Cfg.DiagnoseDevMenu = v.GetBoolean();
|
||||||
if (r.TryGetProperty("QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
|
if (TryGetConfigValue(r, "QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32();
|
||||||
if (r.TryGetProperty("QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array)
|
if (TryGetConfigValue(r, "QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
var rooms = new List<string>();
|
var rooms = new List<string>();
|
||||||
foreach (var e in v.EnumerateArray())
|
foreach (var e in v.EnumerateArray())
|
||||||
@@ -653,8 +643,8 @@ public class Mod : MelonMod
|
|||||||
rooms.Add(e.GetString()!);
|
rooms.Add(e.GetString()!);
|
||||||
Cfg.QuestMaxTeamSizeRooms = rooms.ToArray(); // explicit (incl. empty = none)
|
Cfg.QuestMaxTeamSizeRooms = rooms.ToArray(); // explicit (incl. empty = none)
|
||||||
}
|
}
|
||||||
if (r.TryGetProperty("EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
if (TryGetConfigValue(r, "EnableDevWatchButton", out v)) Cfg.EnableDevWatchButton = v.GetBoolean();
|
||||||
if (r.TryGetProperty("DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
if (TryGetConfigValue(r, "DevCommands", out v) && v.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
var list = new List<string>();
|
var list = new List<string>();
|
||||||
foreach (var e in v.EnumerateArray())
|
foreach (var e in v.EnumerateArray())
|
||||||
@@ -666,7 +656,7 @@ public class Mod : MelonMod
|
|||||||
}
|
}
|
||||||
// "InjectAuthValues" key in the template is now ignored —
|
// "InjectAuthValues" key in the template is now ignored —
|
||||||
// see attic/AuthValuesInjector.cs.attic.
|
// see attic/AuthValuesInjector.cs.attic.
|
||||||
Log.Msg($"[config] loaded: ServerHost={Cfg.ServerHost}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "<unset>" : "<set>")}, " +
|
Log.Msg($"[config] loaded from {path}: ServerHost={Cfg.ServerHost}, PhotonAppId={(string.IsNullOrEmpty(Cfg.PhotonAppId) ? "<unset>" : "<set>")}, " +
|
||||||
$"EnableTlsTrustBypass={Cfg.EnableTlsTrustBypass}");
|
$"EnableTlsTrustBypass={Cfg.EnableTlsTrustBypass}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -675,6 +665,44 @@ public class Mod : MelonMod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ResolveUserDataDirectory()
|
||||||
|
{
|
||||||
|
foreach (var typeName in new[]
|
||||||
|
{
|
||||||
|
"MelonLoader.MelonEnvironment, MelonLoader",
|
||||||
|
"MelonLoader.MelonUtils, MelonLoader",
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var type = Type.GetType(typeName);
|
||||||
|
var prop = type?.GetProperty("UserDataDirectory", BindingFlags.Public | BindingFlags.Static);
|
||||||
|
if (prop?.GetValue(null) is string value && !string.IsNullOrWhiteSpace(value))
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var modDll = Assembly.GetExecutingAssembly().Location;
|
||||||
|
var modsDir = Path.GetDirectoryName(modDll) ?? string.Empty;
|
||||||
|
var gameDir = Path.GetDirectoryName(modsDir) ?? AppContext.BaseDirectory;
|
||||||
|
return Path.Combine(gameDir, "MelonLoader", "UserData");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetConfigValue(JsonElement root, string name, out JsonElement value)
|
||||||
|
{
|
||||||
|
if (root.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
foreach (var prop in root.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
value = prop.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Patch dispatch helpers ────────────────────────────────────────
|
// ── Patch dispatch helpers ────────────────────────────────────────
|
||||||
private bool TryPatch(string label, Type target, string methodName,
|
private bool TryPatch(string label, Type target, string methodName,
|
||||||
Type[]? args, string? prefix = null, string? postfix = null,
|
Type[]? args, string? prefix = null, string? postfix = null,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
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)
|
||||||
|
.Append(AdminHost)
|
||||||
|
.Append("feed")
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Order(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
public static bool IsInfrastructurePath(string path)
|
||||||
|
{
|
||||||
|
return path.Equals("/healthz", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| path.StartsWith("/internal/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolvePublicService(string? host, string path, string apexDomain)
|
||||||
|
{
|
||||||
|
var normalizedPath = NormalizePath(path);
|
||||||
|
var subdomain = Subdomain(host, apexDomain);
|
||||||
|
|
||||||
|
if (string.Equals(subdomain, AdminHost, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return ServiceNames.Web;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in ServiceRouteGroups.Where(group => group.ServiceName != ServiceNames.Web))
|
||||||
|
{
|
||||||
|
if (MatchesHost(subdomain, group.HostSubdomains))
|
||||||
|
{
|
||||||
|
return group.ServiceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in ServiceRouteGroups)
|
||||||
|
{
|
||||||
|
if (StartsWithAny(normalizedPath, group.PathPrefixes))
|
||||||
|
{
|
||||||
|
return group.ServiceName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var web = ServiceRouteGroups.First(group => group.ServiceName == ServiceNames.Web);
|
||||||
|
if ((subdomain is null && web.IncludesApexHost) || MatchesHost(subdomain, web.HostSubdomains))
|
||||||
|
{
|
||||||
|
return ServiceNames.Web;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServiceNames.Monolith;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsOwnedBy(string serviceName, string? host, string path, string apexDomain)
|
||||||
|
{
|
||||||
|
if (IsInfrastructurePath(path))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Equals(
|
||||||
|
ResolvePublicService(host, path, apexDomain),
|
||||||
|
serviceName,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? Subdomain(string? host, string apexDomain)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(host))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedHost = host.Split(':', 2)[0].Trim().ToLowerInvariant();
|
||||||
|
var normalizedApex = apexDomain.Split(':', 2)[0].Trim().ToLowerInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedHost)
|
||||||
|
|| string.IsNullOrWhiteSpace(normalizedApex)
|
||||||
|
|| string.Equals(normalizedHost, normalizedApex, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var suffix = "." + normalizedApex;
|
||||||
|
if (normalizedHost.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return normalizedHost[..^suffix.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
var dot = normalizedHost.IndexOf('.');
|
||||||
|
return dot > 0 ? normalizedHost[..dot] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool StartsWithAny(string path, IEnumerable<string> prefixes)
|
||||||
|
{
|
||||||
|
foreach (var prefix in prefixes)
|
||||||
|
{
|
||||||
|
if (StartsWithPathSegment(path, prefix))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool StartsWithPathSegment(string path, string prefix)
|
||||||
|
{
|
||||||
|
var normalizedPath = NormalizePath(path);
|
||||||
|
var normalizedPrefix = NormalizePath(prefix).TrimEnd('/');
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedPrefix))
|
||||||
|
{
|
||||||
|
normalizedPrefix = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedPath.Equals(normalizedPrefix, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| normalizedPath.StartsWith(normalizedPrefix + "/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesHost(string? subdomain, IEnumerable<string> candidates)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(subdomain))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates.Contains(subdomain, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizePath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return path[0] == '/' ? path : "/" + path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record DorkNetRouteGroup(
|
||||||
|
string ServiceName,
|
||||||
|
string[] HostSubdomains,
|
||||||
|
string[] PathPrefixes,
|
||||||
|
bool IncludesApexHost = false);
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
namespace DorkNet.Contracts;
|
||||||
|
|
||||||
|
public sealed class DorkNetServiceMapOptions
|
||||||
|
{
|
||||||
|
public string Identity { get; set; } = "http://localhost:8081";
|
||||||
|
public string Rooms { get; set; } = "http://localhost:8082";
|
||||||
|
public string Notify { get; set; } = "http://localhost:8083";
|
||||||
|
public string Content { get; set; } = "http://localhost:8084";
|
||||||
|
public string Social { get; set; } = "http://localhost:8085";
|
||||||
|
public string Commerce { get; set; } = "http://localhost:8086";
|
||||||
|
public string Platform { get; set; } = "http://localhost:8087";
|
||||||
|
public string Moderation { get; set; } = "http://localhost:8088";
|
||||||
|
public string Web { get; set; } = "http://localhost:8089";
|
||||||
|
public string Monolith { get; set; } = "http://localhost:8090";
|
||||||
|
|
||||||
|
public IReadOnlyList<DorkNetServiceEndpoint> Endpoints()
|
||||||
|
{
|
||||||
|
DorkNetServiceEndpoint[] endpoints =
|
||||||
|
[
|
||||||
|
new(ServiceNames.Identity, Identity),
|
||||||
|
new(ServiceNames.Rooms, Rooms),
|
||||||
|
new(ServiceNames.Notify, Notify),
|
||||||
|
new(ServiceNames.Content, Content),
|
||||||
|
new(ServiceNames.Social, Social),
|
||||||
|
new(ServiceNames.Commerce, Commerce),
|
||||||
|
new(ServiceNames.Platform, Platform),
|
||||||
|
new(ServiceNames.Moderation, Moderation),
|
||||||
|
new(ServiceNames.Web, Web),
|
||||||
|
new(ServiceNames.Monolith, Monolith),
|
||||||
|
];
|
||||||
|
|
||||||
|
return endpoints
|
||||||
|
.Where(endpoint => !string.IsNullOrWhiteSpace(endpoint.BaseUrl))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record DorkNetServiceEndpoint(string Name, string BaseUrl);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace DorkNet.Contracts;
|
||||||
|
|
||||||
|
public sealed record ServiceHealthResponse(
|
||||||
|
string Service,
|
||||||
|
string Status,
|
||||||
|
DateTimeOffset CheckedAtUtc);
|
||||||
|
|
||||||
|
public sealed record ServiceCapabilityResponse(
|
||||||
|
string Service,
|
||||||
|
string[] Owns,
|
||||||
|
string[] PlannedPublicRoutes);
|
||||||
|
|
||||||
|
public sealed record ServiceProbeResponse(
|
||||||
|
string Service,
|
||||||
|
string BaseUrl,
|
||||||
|
string Status,
|
||||||
|
int? StatusCode,
|
||||||
|
string? Error,
|
||||||
|
DateTimeOffset CheckedAtUtc);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace DorkNet.Contracts;
|
||||||
|
|
||||||
|
public static class ServiceNames
|
||||||
|
{
|
||||||
|
public const string Gateway = "gateway";
|
||||||
|
public const string Identity = "identity";
|
||||||
|
public const string Rooms = "rooms";
|
||||||
|
public const string Notify = "notify";
|
||||||
|
public const string Content = "content";
|
||||||
|
public const string Social = "social";
|
||||||
|
public const string Commerce = "commerce";
|
||||||
|
public const string Platform = "platform";
|
||||||
|
public const string Moderation = "moderation";
|
||||||
|
public const string Web = "web";
|
||||||
|
public const string Monolith = "monolith";
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Yarp.ReverseProxy.Configuration;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Gateway);
|
||||||
|
builder.Services.Configure<DorkNetServiceMapOptions>(
|
||||||
|
builder.Configuration.GetSection("DorkNet:Services"));
|
||||||
|
var serviceMap = builder.Configuration
|
||||||
|
.GetSection("DorkNet:Services")
|
||||||
|
.Get<DorkNetServiceMapOptions>() ?? new DorkNetServiceMapOptions();
|
||||||
|
var apexDomain = builder.Configuration["Domain:Apex"]
|
||||||
|
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
|
||||||
|
?? "localhost";
|
||||||
|
builder.Services
|
||||||
|
.AddReverseProxy()
|
||||||
|
.LoadFromMemory(
|
||||||
|
BuildRoutes(apexDomain, serviceMap),
|
||||||
|
BuildClusters(serviceMap));
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.MapDorkNetServiceDefaults();
|
||||||
|
|
||||||
|
var publicHosts = BuildPublicHosts(apexDomain);
|
||||||
|
app.Use(async (ctx, next) =>
|
||||||
|
{
|
||||||
|
if (DorkNetRouteOwnership.IsInfrastructurePath(ctx.Request.Path.Value ?? "/")
|
||||||
|
|| IsAllowedPublicHost(ctx.Request.Host.Host, publicHosts))
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||||
|
await ctx.Response.WriteAsync("Unknown DorkNet host.");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/internal/services", (IOptions<DorkNetServiceMapOptions> options) =>
|
||||||
|
Results.Ok(options.Value.Endpoints()));
|
||||||
|
app.MapGet("/internal/routes", () =>
|
||||||
|
Results.Ok(new
|
||||||
|
{
|
||||||
|
ApexDomain = apexDomain,
|
||||||
|
Routes = BuildRoutes(apexDomain, serviceMap).Select(r => new
|
||||||
|
{
|
||||||
|
r.RouteId,
|
||||||
|
r.ClusterId,
|
||||||
|
r.Match.Hosts,
|
||||||
|
r.Match.Path,
|
||||||
|
r.Order,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.MapGet("/internal/services/health", async (
|
||||||
|
IOptions<DorkNetServiceMapOptions> options,
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var client = httpClientFactory.CreateClient();
|
||||||
|
var probes = new List<ServiceProbeResponse>();
|
||||||
|
|
||||||
|
foreach (var endpoint in options.Value.Endpoints())
|
||||||
|
{
|
||||||
|
probes.Add(await ProbeServiceAsync(client, endpoint, ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok(probes);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapReverseProxy();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
static async Task<ServiceProbeResponse> ProbeServiceAsync(
|
||||||
|
HttpClient client,
|
||||||
|
DorkNetServiceEndpoint endpoint,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var checkedAt = DateTimeOffset.UtcNow;
|
||||||
|
var baseUrl = endpoint.BaseUrl.TrimEnd('/');
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await client.GetAsync($"{baseUrl}/internal/healthz", ct);
|
||||||
|
return new ServiceProbeResponse(
|
||||||
|
endpoint.Name,
|
||||||
|
endpoint.BaseUrl,
|
||||||
|
response.IsSuccessStatusCode ? "ok" : "unhealthy",
|
||||||
|
(int)response.StatusCode,
|
||||||
|
null,
|
||||||
|
checkedAt);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
|
||||||
|
{
|
||||||
|
return new ServiceProbeResponse(
|
||||||
|
endpoint.Name,
|
||||||
|
endpoint.BaseUrl,
|
||||||
|
"unreachable",
|
||||||
|
null,
|
||||||
|
ex.Message,
|
||||||
|
checkedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static IReadOnlyList<RouteConfig> BuildRoutes(
|
||||||
|
string apexDomain,
|
||||||
|
DorkNetServiceMapOptions services)
|
||||||
|
{
|
||||||
|
var routes = new List<RouteConfig>();
|
||||||
|
var order = 0;
|
||||||
|
var webGroup = DorkNetRouteOwnership.RouteGroups
|
||||||
|
.First(group => group.ServiceName == ServiceNames.Web);
|
||||||
|
|
||||||
|
AddHostRoute(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
webGroup.ServiceName,
|
||||||
|
BaseUrlFor(services, webGroup.ServiceName),
|
||||||
|
"web-admin-host",
|
||||||
|
[SubdomainHost("admin", apexDomain)]);
|
||||||
|
|
||||||
|
AddHostRoute(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
webGroup.ServiceName,
|
||||||
|
BaseUrlFor(services, webGroup.ServiceName),
|
||||||
|
"web-feed-host",
|
||||||
|
[SubdomainHost("feed", apexDomain)]);
|
||||||
|
|
||||||
|
AddHostPathRoutes(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
webGroup.ServiceName,
|
||||||
|
BaseUrlFor(services, webGroup.ServiceName),
|
||||||
|
WebBrowserHosts(apexDomain),
|
||||||
|
[
|
||||||
|
"/admin",
|
||||||
|
"/assets",
|
||||||
|
"/brand",
|
||||||
|
"/feed",
|
||||||
|
"/join",
|
||||||
|
"/p",
|
||||||
|
"/photo",
|
||||||
|
"/players",
|
||||||
|
"/rooms",
|
||||||
|
"/u",
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach (var group in DorkNetRouteOwnership.RouteGroups.Where(group => group.ServiceName != ServiceNames.Web))
|
||||||
|
{
|
||||||
|
AddHostRoute(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
group.ServiceName,
|
||||||
|
BaseUrlFor(services, group.ServiceName),
|
||||||
|
$"{group.ServiceName}-hosts",
|
||||||
|
group.HostSubdomains.Select(subdomain => SubdomainHost(subdomain, apexDomain)).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in DorkNetRouteOwnership.RouteGroups)
|
||||||
|
{
|
||||||
|
AddPathRoutes(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
group.ServiceName,
|
||||||
|
BaseUrlFor(services, group.ServiceName),
|
||||||
|
group.PathPrefixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddHostRoute(
|
||||||
|
routes,
|
||||||
|
ref order,
|
||||||
|
webGroup.ServiceName,
|
||||||
|
BaseUrlFor(services, webGroup.ServiceName),
|
||||||
|
"web-hosts",
|
||||||
|
BuildWebHosts(webGroup, apexDomain));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(services.Monolith))
|
||||||
|
{
|
||||||
|
routes.Add(Route(ServiceNames.Monolith, "monolith-fallback", "/{**catch-all}", order++));
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static IReadOnlyList<ClusterConfig> BuildClusters(DorkNetServiceMapOptions services)
|
||||||
|
{
|
||||||
|
var clusters = new List<ClusterConfig>();
|
||||||
|
foreach (var endpoint in services.Endpoints())
|
||||||
|
{
|
||||||
|
AddCluster(clusters, endpoint.Name, endpoint.BaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clusters;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string[] WebBrowserHosts(string apexDomain)
|
||||||
|
{
|
||||||
|
return [apexDomain, SubdomainHost("www", apexDomain)];
|
||||||
|
}
|
||||||
|
|
||||||
|
static HashSet<string> BuildPublicHosts(string apexDomain)
|
||||||
|
{
|
||||||
|
var hosts = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
apexDomain,
|
||||||
|
"localhost",
|
||||||
|
"127.0.0.1",
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var subdomain in DorkNetRouteOwnership.PublicSubdomains)
|
||||||
|
{
|
||||||
|
hosts.Add(SubdomainHost(subdomain, apexDomain));
|
||||||
|
}
|
||||||
|
|
||||||
|
return hosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsAllowedPublicHost(string? host, HashSet<string> publicHosts)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(host))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return publicHosts.Contains(host.Split(':', 2)[0].Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AddHostRoute(
|
||||||
|
List<RouteConfig> routes,
|
||||||
|
ref int order,
|
||||||
|
string serviceName,
|
||||||
|
string baseUrl,
|
||||||
|
string routeId,
|
||||||
|
string[] hosts)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(baseUrl) || hosts.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
routes.Add(Route(
|
||||||
|
serviceName,
|
||||||
|
routeId,
|
||||||
|
"/{**catch-all}",
|
||||||
|
order++,
|
||||||
|
hosts));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AddPathRoutes(
|
||||||
|
List<RouteConfig> routes,
|
||||||
|
ref int order,
|
||||||
|
string serviceName,
|
||||||
|
string baseUrl,
|
||||||
|
IEnumerable<string> paths)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var path in paths)
|
||||||
|
{
|
||||||
|
var routeId = $"{serviceName}-{path.Trim('/').Replace('/', '-').Replace("{**catch-all}", "all")}";
|
||||||
|
var exactPath = NormalizeRoutePrefix(path);
|
||||||
|
routes.Add(Route(serviceName, $"{routeId}-exact", exactPath, order++));
|
||||||
|
routes.Add(Route(serviceName, routeId, $"{exactPath}/{{**catch-all}}", order++));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AddHostPathRoutes(
|
||||||
|
List<RouteConfig> routes,
|
||||||
|
ref int order,
|
||||||
|
string serviceName,
|
||||||
|
string baseUrl,
|
||||||
|
string[] hosts,
|
||||||
|
IEnumerable<string> paths)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(baseUrl) || hosts.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var path in paths)
|
||||||
|
{
|
||||||
|
var routeId = $"{serviceName}-browser-{path.Trim('/').Replace('/', '-').Replace("{**catch-all}", "all")}";
|
||||||
|
var exactPath = NormalizeRoutePrefix(path);
|
||||||
|
routes.Add(Route(serviceName, $"{routeId}-exact", exactPath, order++, hosts));
|
||||||
|
routes.Add(Route(serviceName, routeId, $"{exactPath}/{{**catch-all}}", order++, hosts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string BaseUrlFor(DorkNetServiceMapOptions services, string serviceName)
|
||||||
|
{
|
||||||
|
return serviceName switch
|
||||||
|
{
|
||||||
|
ServiceNames.Identity => services.Identity,
|
||||||
|
ServiceNames.Rooms => services.Rooms,
|
||||||
|
ServiceNames.Notify => services.Notify,
|
||||||
|
ServiceNames.Content => services.Content,
|
||||||
|
ServiceNames.Social => services.Social,
|
||||||
|
ServiceNames.Commerce => services.Commerce,
|
||||||
|
ServiceNames.Platform => services.Platform,
|
||||||
|
ServiceNames.Moderation => services.Moderation,
|
||||||
|
ServiceNames.Web => services.Web,
|
||||||
|
ServiceNames.Monolith => services.Monolith,
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static string[] BuildWebHosts(DorkNetRouteGroup group, string apexDomain)
|
||||||
|
{
|
||||||
|
var hosts = new List<string>();
|
||||||
|
if (group.IncludesApexHost)
|
||||||
|
{
|
||||||
|
hosts.Add(apexDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts.AddRange(group.HostSubdomains
|
||||||
|
.Where(subdomain => !string.Equals(subdomain, "admin", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(subdomain => SubdomainHost(subdomain, apexDomain)));
|
||||||
|
|
||||||
|
return hosts.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string SubdomainHost(string subdomain, string apexDomain)
|
||||||
|
{
|
||||||
|
return $"{subdomain}.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
static string NormalizeRoutePrefix(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || path == "/")
|
||||||
|
{
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (path[0] == '/' ? path : "/" + path).TrimEnd('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
static RouteConfig Route(
|
||||||
|
string clusterId,
|
||||||
|
string routeId,
|
||||||
|
string path,
|
||||||
|
int order,
|
||||||
|
string[]? hosts = null)
|
||||||
|
{
|
||||||
|
return new RouteConfig
|
||||||
|
{
|
||||||
|
RouteId = routeId,
|
||||||
|
ClusterId = clusterId,
|
||||||
|
Order = order,
|
||||||
|
Match = new RouteMatch
|
||||||
|
{
|
||||||
|
Hosts = hosts,
|
||||||
|
Path = path,
|
||||||
|
},
|
||||||
|
Transforms =
|
||||||
|
[
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["RequestHeaderOriginalHost"] = "true",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AddCluster(
|
||||||
|
List<ClusterConfig> clusters,
|
||||||
|
string serviceName,
|
||||||
|
string baseUrl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = baseUrl.EndsWith('/') ? baseUrl : baseUrl + "/";
|
||||||
|
clusters.Add(new ClusterConfig
|
||||||
|
{
|
||||||
|
ClusterId = serviceName,
|
||||||
|
Destinations = new Dictionary<string, DestinationConfig>
|
||||||
|
{
|
||||||
|
["primary"] = new() { Address = address },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"System.Net.Http.HttpClient": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DorkNet": {
|
||||||
|
"Services": {
|
||||||
|
"Identity": "http://localhost:8081",
|
||||||
|
"Rooms": "http://localhost:8082",
|
||||||
|
"Notify": "http://localhost:8083",
|
||||||
|
"Content": "http://localhost:8084",
|
||||||
|
"Social": "http://localhost:8085",
|
||||||
|
"Commerce": "http://localhost:8086",
|
||||||
|
"Platform": "http://localhost:8087",
|
||||||
|
"Moderation": "http://localhost:8088",
|
||||||
|
"Web": "http://localhost:8089",
|
||||||
|
"Monolith": "http://localhost:8090"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.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="10.0.9" />
|
||||||
|
<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,75 @@
|
|||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.HostFiltering;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_dataRoot))
|
||||||
|
{
|
||||||
|
Directory.Delete(_dataRoot, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Test cleanup must not hide the original test failure.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
|
|
||||||
|
namespace DorkNet.Server.Tests;
|
||||||
|
|
||||||
|
public static partial class EndpointContractDiscovery
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<EndpointContract> Discover()
|
||||||
|
{
|
||||||
|
var serverAssembly = typeof(Program).Assembly;
|
||||||
|
var controllerBaseType = typeof(ControllerBase);
|
||||||
|
return serverAssembly.GetTypes()
|
||||||
|
.Where(t => !t.IsAbstract && controllerBaseType.IsAssignableFrom(t))
|
||||||
|
.SelectMany(DiscoverControllerRoutes)
|
||||||
|
.OrderBy(route => route.Method, StringComparer.Ordinal)
|
||||||
|
.ThenBy(route => route.SamplePath, StringComparer.Ordinal)
|
||||||
|
.ThenBy(route => route.Source, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<EndpointContract> DiscoverControllerRoutes(Type controller)
|
||||||
|
{
|
||||||
|
var controllerName = ControllerName(controller);
|
||||||
|
var controllerRouteTemplates = controller.GetCustomAttributes(inherit: true)
|
||||||
|
.OfType<IRouteTemplateProvider>()
|
||||||
|
.Select(a => a.Template)
|
||||||
|
.Where(t => !string.IsNullOrWhiteSpace(t))
|
||||||
|
.Cast<string>()
|
||||||
|
.DefaultIfEmpty("")
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var method in controller.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
|
||||||
|
{
|
||||||
|
var httpAttrs = method.GetCustomAttributes(inherit: true)
|
||||||
|
.OfType<HttpMethodAttribute>()
|
||||||
|
.ToArray();
|
||||||
|
if (httpAttrs.Length == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var attr in httpAttrs)
|
||||||
|
{
|
||||||
|
var attrMethods = attr.HttpMethods.Any()
|
||||||
|
? attr.HttpMethods
|
||||||
|
: new[] { "ANY" };
|
||||||
|
|
||||||
|
foreach (var httpMethod in attrMethods)
|
||||||
|
{
|
||||||
|
foreach (var controllerTemplate in controllerRouteTemplates)
|
||||||
|
{
|
||||||
|
var template = CombineTemplates(controllerTemplate, attr.Template);
|
||||||
|
template = ReplaceRouteTokens(template, controllerName, method.Name);
|
||||||
|
yield return new EndpointContract(
|
||||||
|
httpMethod.ToUpperInvariant(),
|
||||||
|
template,
|
||||||
|
ToSamplePath(template),
|
||||||
|
$"{controller.FullName}.{method.Name}",
|
||||||
|
RequiresAuthorization(controller, method),
|
||||||
|
IsAdminEndpoint(controller, method),
|
||||||
|
ConsumesContentTypes(controller, method));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RequiresAuthorization(Type controller, MethodInfo method)
|
||||||
|
{
|
||||||
|
var attributes = controller.GetCustomAttributes(inherit: true)
|
||||||
|
.Concat(method.GetCustomAttributes(inherit: true))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (attributes.OfType<IAllowAnonymous>().Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes.OfType<IAuthorizeData>().Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAdminEndpoint(Type controller, MethodInfo method)
|
||||||
|
{
|
||||||
|
return controller.FullName?.Contains(".Controllers.Admin.", StringComparison.Ordinal) == true
|
||||||
|
|| method.Name.Contains("Admin", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] ConsumesContentTypes(Type controller, MethodInfo method)
|
||||||
|
{
|
||||||
|
var consumes = method.GetCustomAttributes(inherit: true)
|
||||||
|
.Concat(controller.GetCustomAttributes(inherit: true))
|
||||||
|
.OfType<ConsumesAttribute>()
|
||||||
|
.SelectMany(a => a.ContentTypes)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return consumes.Length == 0
|
||||||
|
? ["application/json"]
|
||||||
|
: consumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ControllerName(Type controller)
|
||||||
|
{
|
||||||
|
const string suffix = "Controller";
|
||||||
|
return controller.Name.EndsWith(suffix, StringComparison.Ordinal)
|
||||||
|
? controller.Name[..^suffix.Length]
|
||||||
|
: controller.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CombineTemplates(string? controllerTemplate, string? actionTemplate)
|
||||||
|
{
|
||||||
|
controllerTemplate ??= "";
|
||||||
|
actionTemplate ??= "";
|
||||||
|
|
||||||
|
if (actionTemplate.StartsWith("~/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return "/" + actionTemplate[2..].TrimStart('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionTemplate.StartsWith("/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return "/" + actionTemplate.TrimStart('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
var left = controllerTemplate.Trim('/');
|
||||||
|
var right = actionTemplate.Trim('/');
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(left) && string.IsNullOrEmpty(right))
|
||||||
|
{
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(left))
|
||||||
|
{
|
||||||
|
return "/" + right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(right))
|
||||||
|
{
|
||||||
|
return "/" + left;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/" + left + "/" + right;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReplaceRouteTokens(string template, string controllerName, string actionName)
|
||||||
|
{
|
||||||
|
return template
|
||||||
|
.Replace("[controller]", controllerName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("[action]", actionName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToSamplePath(string template)
|
||||||
|
{
|
||||||
|
var path = RouteParameterRegex().Replace(template, match =>
|
||||||
|
{
|
||||||
|
var name = match.Groups["name"].Value.ToLowerInvariant();
|
||||||
|
var constraint = match.Groups["constraint"].Value.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (name.Contains("playerid") || name.Contains("accountid") || name == "id")
|
||||||
|
{
|
||||||
|
return EndpointContract.TestPlayerToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraint.Contains("long") || constraint.Contains("int") || name.EndsWith("id"))
|
||||||
|
{
|
||||||
|
return "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraint.Contains("bool"))
|
||||||
|
{
|
||||||
|
return "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraint.Contains("guid"))
|
||||||
|
{
|
||||||
|
return "00000000-0000-0000-0000-000000000001";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.Contains("roomname"))
|
||||||
|
{
|
||||||
|
return "DormRoom";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.Contains("locale"))
|
||||||
|
{
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "sample";
|
||||||
|
});
|
||||||
|
|
||||||
|
path = Regex.Replace(path, "/+", "/");
|
||||||
|
return path.StartsWith('/') ? path : "/" + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\{[*]?(?<name>[^}:=\?]+)(?<constraint>:[^}=\?]+)?(?<optional>\?)?(?<default>=[^}]+)?\}")]
|
||||||
|
private static partial Regex RouteParameterRegex();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record EndpointContract(
|
||||||
|
string Method,
|
||||||
|
string Template,
|
||||||
|
string SamplePath,
|
||||||
|
string Source,
|
||||||
|
bool RequiresAuthorization,
|
||||||
|
bool IsAdminEndpoint,
|
||||||
|
string[] Consumes)
|
||||||
|
{
|
||||||
|
public const string TestPlayerToken = "__test_player_id__";
|
||||||
|
|
||||||
|
public string PathFor(GameClientSession session)
|
||||||
|
{
|
||||||
|
return SamplePath.Replace(
|
||||||
|
TestPlayerToken,
|
||||||
|
session.PlayerId.ToString(),
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace DorkNet.Server.Tests;
|
||||||
|
|
||||||
|
public sealed class GameClientEndpointContractTests :
|
||||||
|
IClassFixture<DorkNetServerFactory>
|
||||||
|
{
|
||||||
|
private readonly DorkNetServerFactory _factory;
|
||||||
|
|
||||||
|
public GameClientEndpointContractTests(DorkNetServerFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Game_client_contract_probe_reaches_every_controller_route()
|
||||||
|
{
|
||||||
|
var routes = EndpointContractDiscovery.Discover();
|
||||||
|
Assert.NotEmpty(routes);
|
||||||
|
|
||||||
|
using var client = _factory.CreateClient(new()
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = false,
|
||||||
|
});
|
||||||
|
client.BaseAddress = new Uri($"http://api.{_factory.ApexDomain}");
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(20);
|
||||||
|
|
||||||
|
var session = await GameClientSessionFactory.CreateAsync(client, _factory.ApexDomain);
|
||||||
|
var failures = new List<string>();
|
||||||
|
|
||||||
|
foreach (var route in routes)
|
||||||
|
{
|
||||||
|
var result = await ProbeAsync(client, route, session, _factory.ApexDomain);
|
||||||
|
if (!result.Passed)
|
||||||
|
{
|
||||||
|
failures.Add(
|
||||||
|
$"{route.Method,-6} {result.Host}{result.Path} {route.Source} expected {result.Expected}, got {result.Observed}"
|
||||||
|
+ (string.IsNullOrWhiteSpace(result.ResponseExcerpt) ? "" : $" body={result.ResponseExcerpt}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
failures.Count == 0,
|
||||||
|
$"Endpoint contract failures ({failures.Count}/{routes.Count}):{Environment.NewLine}"
|
||||||
|
+ string.Join(Environment.NewLine, failures));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<EndpointProbeResult> ProbeAsync(
|
||||||
|
HttpClient client,
|
||||||
|
EndpointContract route,
|
||||||
|
GameClientSession session,
|
||||||
|
string apexDomain)
|
||||||
|
{
|
||||||
|
var path = route.PathFor(session);
|
||||||
|
path = AddClientQuery(path, route, session);
|
||||||
|
var host = HostFor(path, apexDomain);
|
||||||
|
using var request = new HttpRequestMessage(
|
||||||
|
ToHttpMethod(route.Method),
|
||||||
|
new Uri($"http://{host}{path}"));
|
||||||
|
request.Headers.Accept.ParseAdd("application/json");
|
||||||
|
request.Headers.UserAgent.ParseAdd("RecRoom/2020.12.18");
|
||||||
|
request.Headers.TryAddWithoutValidation("X-DorkNet-Version", "december_2020_12_18");
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", session.AccessToken);
|
||||||
|
|
||||||
|
if (NeedsRequestBody(request.Method))
|
||||||
|
{
|
||||||
|
request.Content = CreateContent(route, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await client.SendAsync(request);
|
||||||
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
var statusCode = (int)response.StatusCode;
|
||||||
|
var passed = ResponseMatchesContract(route, response.StatusCode);
|
||||||
|
return new EndpointProbeResult(
|
||||||
|
route.Method,
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
ExpectedContract(route),
|
||||||
|
statusCode,
|
||||||
|
null,
|
||||||
|
Excerpt(responseText),
|
||||||
|
passed);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
|
||||||
|
{
|
||||||
|
return new EndpointProbeResult(
|
||||||
|
route.Method,
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
ExpectedContract(route),
|
||||||
|
null,
|
||||||
|
ex.Message,
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ResponseMatchesContract(
|
||||||
|
EndpointContract route,
|
||||||
|
HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
if ((int)statusCode >= 500)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode is HttpStatusCode.MethodNotAllowed
|
||||||
|
or HttpStatusCode.NotImplemented
|
||||||
|
or HttpStatusCode.UpgradeRequired)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.RequiresAuthorization
|
||||||
|
&& statusCode is HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExpectedContract(EndpointContract route)
|
||||||
|
{
|
||||||
|
if (route.IsAdminEndpoint)
|
||||||
|
{
|
||||||
|
return "admin endpoint may reject non-admin test player, but must not 5xx/405/426";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.RequiresAuthorization)
|
||||||
|
{
|
||||||
|
return "authenticated test player should reach the action without 401/5xx/405/426; 403 is valid for owner/admin-only resources";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "game-client request should not 5xx/405/426";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpMethod ToHttpMethod(string method)
|
||||||
|
{
|
||||||
|
return string.Equals(method, "ANY", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? HttpMethod.Get
|
||||||
|
: new HttpMethod(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool NeedsRequestBody(HttpMethod method)
|
||||||
|
{
|
||||||
|
return method == HttpMethod.Post
|
||||||
|
|| method == HttpMethod.Put
|
||||||
|
|| method.Method.Equals("PATCH", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpContent CreateContent(
|
||||||
|
EndpointContract route,
|
||||||
|
GameClientSession session)
|
||||||
|
{
|
||||||
|
var consumes = route.Consumes.Select(c => c.ToLowerInvariant()).ToArray();
|
||||||
|
if (consumes.Any(c => c.Contains("application/x-www-form-urlencoded")))
|
||||||
|
{
|
||||||
|
return new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["account_id"] = session.PlayerId.ToString(),
|
||||||
|
["AccountId"] = session.PlayerId.ToString(),
|
||||||
|
["clubId"] = "1",
|
||||||
|
["ClubId"] = "1",
|
||||||
|
["description"] = "Endpoint contract description",
|
||||||
|
["deviceId"] = session.DeviceId,
|
||||||
|
["grant_type"] = "password",
|
||||||
|
["image"] = "endpoint-contract.png",
|
||||||
|
["imageName"] = "endpoint-contract.png",
|
||||||
|
["fileName"] = "endpoint-contract.png",
|
||||||
|
["message"] = "Endpoint contract message",
|
||||||
|
["name"] = "EndpointContract",
|
||||||
|
["newPassword"] = "endpoint-contract-password",
|
||||||
|
["oldPassword"] = "",
|
||||||
|
["password"] = "endpoint-contract-password",
|
||||||
|
["ping"] = "42",
|
||||||
|
["platformId"] = session.PlatformId,
|
||||||
|
["playerId"] = session.PlayerId.ToString(),
|
||||||
|
["playerIds"] = session.PlayerId.ToString(),
|
||||||
|
["query"] = "DormRoom",
|
||||||
|
["region"] = "us",
|
||||||
|
["targetPlayerId"] = session.PlayerId.ToString(),
|
||||||
|
["text"] = "Endpoint contract message",
|
||||||
|
["username"] = "EndpointContract",
|
||||||
|
["device_id"] = session.DeviceId,
|
||||||
|
["platform"] = "0",
|
||||||
|
["platform_id"] = session.PlatformId,
|
||||||
|
["Ids"] = session.PlayerId.ToString(),
|
||||||
|
["PlayerIds"] = session.PlayerId.ToString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consumes.Any(c => c.Contains("multipart/form-data")))
|
||||||
|
{
|
||||||
|
var content = new MultipartFormDataContent();
|
||||||
|
content.Add(new StringContent("endpoint-contract"), "name");
|
||||||
|
content.Add(new StringContent(session.PlayerId.ToString()), "playerId");
|
||||||
|
content.Add(new StringContent("endpoint-contract.png"), "imageName");
|
||||||
|
content.Add(new StringContent("endpoint-contract.png"), "fileName");
|
||||||
|
content.Add(new ByteArrayContent([]), "file", "endpoint-contract.bin");
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consumes.Any(c => c.Contains("text/plain")))
|
||||||
|
{
|
||||||
|
return new StringContent("", Encoding.UTF8, "text/plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
PlayerId = session.PlayerId,
|
||||||
|
AccountId = session.PlayerId,
|
||||||
|
ClubId = 1,
|
||||||
|
Description = "Endpoint contract description",
|
||||||
|
DeviceId = session.DeviceId,
|
||||||
|
FileName = "endpoint-contract.png",
|
||||||
|
Id = session.PlayerId,
|
||||||
|
Image = "endpoint-contract.png",
|
||||||
|
ImageName = "endpoint-contract.png",
|
||||||
|
Message = "Endpoint contract message",
|
||||||
|
Platform = 0,
|
||||||
|
PlatformId = session.PlatformId,
|
||||||
|
PlayerIds = new[] { session.PlayerId },
|
||||||
|
Name = "EndpointContract",
|
||||||
|
Password = "endpoint-contract-password",
|
||||||
|
Query = "DormRoom",
|
||||||
|
RoomName = "DormRoom",
|
||||||
|
TargetPlayerId = session.PlayerId,
|
||||||
|
Text = "Endpoint contract message",
|
||||||
|
});
|
||||||
|
return new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string AddClientQuery(
|
||||||
|
string path,
|
||||||
|
EndpointContract route,
|
||||||
|
GameClientSession session)
|
||||||
|
{
|
||||||
|
var separator = path.Contains('?', StringComparison.Ordinal) ? "&" : "?";
|
||||||
|
var lower = path.ToLowerInvariant();
|
||||||
|
var query = new List<string>();
|
||||||
|
|
||||||
|
if (lower.Contains("search", StringComparison.Ordinal)
|
||||||
|
|| lower.Contains("query", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
query.Add("query=EndpointContract");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower == "/members/bulk")
|
||||||
|
{
|
||||||
|
query.Add("clubId=1");
|
||||||
|
query.Add($"playerIds={session.PlayerId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower == "/player/photonregionpings")
|
||||||
|
{
|
||||||
|
query.Add("region=us");
|
||||||
|
query.Add("ping=42");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.Source.Contains("CachedLogins", StringComparison.Ordinal)
|
||||||
|
|| lower.Contains("platformlogin/cached", StringComparison.Ordinal)
|
||||||
|
|| lower.Contains("savedlogins", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
query.Add("platform=0");
|
||||||
|
query.Add($"platformId={Uri.EscapeDataString(session.PlatformId)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.Source.Contains("Bulk", StringComparison.Ordinal)
|
||||||
|
&& !query.Any(q => q.StartsWith("Ids=", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
query.Add($"Ids={session.PlayerId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.Count == 0)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path + separator + string.Join("&", query);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HostFor(string path, string apexDomain)
|
||||||
|
{
|
||||||
|
var lower = path.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (lower.StartsWith("/connect/")
|
||||||
|
|| lower.StartsWith("/.well-known/")
|
||||||
|
|| lower.StartsWith("/eac/")
|
||||||
|
|| lower.StartsWith("/cachedlogin/")
|
||||||
|
|| lower.StartsWith("/photon/")
|
||||||
|
|| lower.StartsWith("/api/platformlogin/")
|
||||||
|
|| lower.StartsWith("/api/version")
|
||||||
|
|| lower.StartsWith("/api/auth/"))
|
||||||
|
{
|
||||||
|
return $"auth.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/account/"))
|
||||||
|
{
|
||||||
|
return $"accounts.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/roomserver/")
|
||||||
|
|| lower.StartsWith("/rooms")
|
||||||
|
|| lower.StartsWith("/playlists")
|
||||||
|
|| lower.StartsWith("/featuredrooms")
|
||||||
|
|| lower.StartsWith("/hot_rooms")
|
||||||
|
|| lower.StartsWith("/search_rooms")
|
||||||
|
|| lower.StartsWith("/api/rooms")
|
||||||
|
|| lower.StartsWith("/api/roomkeys")
|
||||||
|
|| lower.StartsWith("/api/gamesessions")
|
||||||
|
|| lower.StartsWith("/api/curatedroomplaylists"))
|
||||||
|
{
|
||||||
|
return $"rooms.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/v1/find")
|
||||||
|
|| lower.StartsWith("/v1/join")
|
||||||
|
|| lower.StartsWith("/v1/leave")
|
||||||
|
|| lower.StartsWith("/v1/session")
|
||||||
|
|| lower.StartsWith("/roominstance/")
|
||||||
|
|| lower.StartsWith("/goto/")
|
||||||
|
|| lower.StartsWith("/player/"))
|
||||||
|
{
|
||||||
|
return $"match.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/thread"))
|
||||||
|
{
|
||||||
|
return $"chat.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/club")
|
||||||
|
|| lower.StartsWith("/announcements/")
|
||||||
|
|| lower.StartsWith("/subscription/")
|
||||||
|
|| lower.StartsWith("/members/")
|
||||||
|
|| lower.StartsWith("/api/clubreporting/"))
|
||||||
|
{
|
||||||
|
return $"clubs.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/api/storefronts/")
|
||||||
|
|| lower.StartsWith("/api/purchase/")
|
||||||
|
|| lower.StartsWith("/api/subscription/"))
|
||||||
|
{
|
||||||
|
return $"commerce.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/hub/") || lower.StartsWith("/api/notification/"))
|
||||||
|
{
|
||||||
|
return $"notify.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/v1/regions"))
|
||||||
|
{
|
||||||
|
return $"geo.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/v1/services") || lower.StartsWith("/v1/ping"))
|
||||||
|
{
|
||||||
|
return $"ns.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/upload"))
|
||||||
|
{
|
||||||
|
return $"storage.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/img/") || lower.StartsWith("/room/") || lower.StartsWith("/data/"))
|
||||||
|
{
|
||||||
|
return $"cdn.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower.StartsWith("/api/admin/"))
|
||||||
|
{
|
||||||
|
return $"admin.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"api.{apexDomain}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Excerpt(string? body)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var compact = body.ReplaceLineEndings(" ").Trim();
|
||||||
|
return compact.Length <= 240 ? compact : compact[..240] + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record EndpointProbeResult(
|
||||||
|
string Method,
|
||||||
|
string Host,
|
||||||
|
string Path,
|
||||||
|
string Expected,
|
||||||
|
int? StatusCode,
|
||||||
|
string? Error,
|
||||||
|
string? ResponseExcerpt,
|
||||||
|
bool Passed)
|
||||||
|
{
|
||||||
|
public string Observed => StatusCode is int code ? code.ToString() : Error ?? "no response";
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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",
|
||||||
|
["client_id"] = "recroom",
|
||||||
|
["client_secret"] = "VxZ53kgbbEaRoZAeMe00MagtgD12GLL2",
|
||||||
|
["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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using Open.IdentityServer;
|
||||||
|
using Open.IdentityServer.Models;
|
||||||
|
using Open.IdentityServer.Services;
|
||||||
|
using Open.IdentityServer.Validation;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using DorkNet.Server.Data;
|
||||||
|
using DorkNet.Server.Data.Entities;
|
||||||
|
using DorkNet.Server.Services;
|
||||||
|
|
||||||
|
namespace DorkNet.Server.Auth;
|
||||||
|
|
||||||
|
public sealed class IdentityServerSigningKeyProvider
|
||||||
|
{
|
||||||
|
private readonly string _legacySecret;
|
||||||
|
|
||||||
|
public IdentityServerSigningKeyProvider(IConfiguration config)
|
||||||
|
{
|
||||||
|
_legacySecret =
|
||||||
|
Environment.GetEnvironmentVariable("DORKNET_JWT_SECRET")
|
||||||
|
?? Environment.GetEnvironmentVariable("RECNET_JWT_SECRET")
|
||||||
|
?? config["Jwt:Secret"]
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"JWT secret not configured. Set DORKNET_JWT_SECRET env var or Jwt:Secret in appsettings.Local.json.");
|
||||||
|
|
||||||
|
CertificatePath = ResolveCertificatePath(config);
|
||||||
|
var password = config["IdentityServer:SigningCertificatePassword"] ?? _legacySecret;
|
||||||
|
var certificateBase64 = config["IdentityServer:SigningCertificateBase64"];
|
||||||
|
if (!string.IsNullOrWhiteSpace(certificateBase64))
|
||||||
|
{
|
||||||
|
LoadedFromBase64 = true;
|
||||||
|
Certificate = LoadCertificateFromBase64(certificateBase64, password);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Certificate = LoadOrCreateCertificate(CertificatePath, password);
|
||||||
|
}
|
||||||
|
SigningKey = new X509SecurityKey(Certificate);
|
||||||
|
LegacyJwtKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_legacySecret));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CertificatePath { get; }
|
||||||
|
public bool LoadedFromBase64 { get; }
|
||||||
|
public string CertificateSource => LoadedFromBase64
|
||||||
|
? "IdentityServer:SigningCertificateBase64"
|
||||||
|
: CertificatePath;
|
||||||
|
public X509Certificate2 Certificate { get; }
|
||||||
|
public SecurityKey SigningKey { get; }
|
||||||
|
public SecurityKey LegacyJwtKey { get; }
|
||||||
|
public SecurityKey[] ValidationKeys => new[] { SigningKey, LegacyJwtKey };
|
||||||
|
|
||||||
|
private static string ResolveCertificatePath(IConfiguration config)
|
||||||
|
{
|
||||||
|
var path = config["IdentityServer:SigningCertificatePath"];
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
path = Path.Combine(AppContext.BaseDirectory, "data", "identityserver-signing.pfx");
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509Certificate2 LoadCertificateFromBase64(string certificateBase64, string password)
|
||||||
|
{
|
||||||
|
var flags = X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.Exportable;
|
||||||
|
var compact = NormalizeCertificateBase64(certificateBase64);
|
||||||
|
byte[] bytes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bytes = Convert.FromBase64String(compact);
|
||||||
|
}
|
||||||
|
catch (FormatException ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"IdentityServer signing certificate base64 is invalid. " +
|
||||||
|
"Set IdentityServer__SigningCertificateBase64 to the PFX bytes encoded as base64, " +
|
||||||
|
"without a file path or shell assignment text.",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new X509Certificate2(bytes, password, flags);
|
||||||
|
}
|
||||||
|
catch (CryptographicException ex)
|
||||||
|
{
|
||||||
|
if (TryLoadPemCertificate(bytes, password, flags, out var certificate))
|
||||||
|
return certificate;
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"IdentityServer signing certificate base64 decoded successfully, but it is not a valid PFX/PKCS#12 certificate. " +
|
||||||
|
"Set IdentityServer__SigningCertificateBase64 to the PFX bytes encoded as base64 and make sure " +
|
||||||
|
"IdentityServer__SigningCertificatePassword matches the PFX password.",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryLoadPemCertificate(
|
||||||
|
byte[] bytes,
|
||||||
|
string password,
|
||||||
|
X509KeyStorageFlags flags,
|
||||||
|
out X509Certificate2 certificate)
|
||||||
|
{
|
||||||
|
certificate = null!;
|
||||||
|
|
||||||
|
string pem;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pem = Encoding.UTF8.GetString(bytes);
|
||||||
|
}
|
||||||
|
catch (DecoderFallbackException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pem.Contains("-----BEGIN", StringComparison.Ordinal))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var pemCertificate = X509Certificate2.CreateFromPem(pem, pem);
|
||||||
|
if (!pemCertificate.HasPrivateKey)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var pfxBytes = pemCertificate.Export(X509ContentType.Pfx, password);
|
||||||
|
certificate = new X509Certificate2(pfxBytes, password, flags);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (CryptographicException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeCertificateBase64(string value)
|
||||||
|
{
|
||||||
|
var text = value.Trim().Trim('"', '\'');
|
||||||
|
var equals = text.IndexOf('=');
|
||||||
|
if (equals > 0)
|
||||||
|
{
|
||||||
|
var key = text[..equals].Trim();
|
||||||
|
if (key.Equals("IdentityServer__SigningCertificateBase64", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
key.Equals("IdentityServer:SigningCertificateBase64", StringComparison.OrdinalIgnoreCase))
|
||||||
|
text = text[(equals + 1)..].Trim().Trim('"', '\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var comma = text.IndexOf(',');
|
||||||
|
if (comma >= 0)
|
||||||
|
text = text[(comma + 1)..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.StartsWith("base64:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
text = text["base64:".Length..];
|
||||||
|
|
||||||
|
var compact = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray())
|
||||||
|
.Replace('-', '+')
|
||||||
|
.Replace('_', '/');
|
||||||
|
return (compact.Length % 4) switch
|
||||||
|
{
|
||||||
|
2 => compact + "==",
|
||||||
|
3 => compact + "=",
|
||||||
|
_ => compact,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509Certificate2 LoadOrCreateCertificate(string path, string password)
|
||||||
|
{
|
||||||
|
var flags = X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.Exportable;
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
|
||||||
|
if (File.Exists(path))
|
||||||
|
return new X509Certificate2(path, password, flags);
|
||||||
|
|
||||||
|
using var rsa = RSA.Create(2048);
|
||||||
|
var request = new CertificateRequest(
|
||||||
|
"CN=DorkNet IdentityServer Signing",
|
||||||
|
rsa,
|
||||||
|
HashAlgorithmName.SHA256,
|
||||||
|
RSASignaturePadding.Pkcs1);
|
||||||
|
request.CertificateExtensions.Add(
|
||||||
|
new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, critical: false));
|
||||||
|
|
||||||
|
using var created = request.CreateSelfSigned(
|
||||||
|
DateTimeOffset.UtcNow.AddDays(-1),
|
||||||
|
DateTimeOffset.UtcNow.AddYears(10));
|
||||||
|
var bytes = created.Export(X509ContentType.Pfx, password);
|
||||||
|
File.WriteAllBytes(path, bytes);
|
||||||
|
return new X509Certificate2(bytes, password, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RecRoomIdentityServerRegistration
|
||||||
|
{
|
||||||
|
public static void AddRecRoomIdentityServer(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IConfiguration config,
|
||||||
|
DomainConfig domain,
|
||||||
|
IdentityServerSigningKeyProvider signingKeyProvider)
|
||||||
|
{
|
||||||
|
services
|
||||||
|
.AddIdentityServer(options =>
|
||||||
|
{
|
||||||
|
options.IssuerUri = domain.AuthIssuer.TrimEnd('/');
|
||||||
|
})
|
||||||
|
.AddInMemoryIdentityResources(RecRoomIdentityServerConfig.IdentityResources)
|
||||||
|
.AddInMemoryApiScopes(RecRoomIdentityServerConfig.ApiScopes)
|
||||||
|
.AddInMemoryClients(RecRoomIdentityServerConfig.Clients)
|
||||||
|
.AddSigningCredential(signingKeyProvider.SigningKey, SecurityAlgorithms.RsaSha256)
|
||||||
|
.AddResourceOwnerValidator<RecRoomPasswordValidator>()
|
||||||
|
.AddExtensionGrantValidator<CachedLoginGrantValidator>()
|
||||||
|
.AddProfileService<RecRoomProfileService>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class RecRoomIdentityServerConfig
|
||||||
|
{
|
||||||
|
public const string ClientId = "recroom";
|
||||||
|
public const string ClientSecret = "VxZ53kgbbEaRoZAeMe00MagtgD12GLL2";
|
||||||
|
public const string GameClientScope = "gameClient";
|
||||||
|
public const string CachedLoginGrantType = "cached_login";
|
||||||
|
|
||||||
|
public static readonly IdentityResource[] IdentityResources =
|
||||||
|
{
|
||||||
|
new IdentityResources.OpenId(),
|
||||||
|
new IdentityResources.Profile(),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly ApiScope[] ApiScopes =
|
||||||
|
{
|
||||||
|
new(GameClientScope, "Game client")
|
||||||
|
{
|
||||||
|
UserClaims = RecRoomIdentityClaims.UserClaimTypes,
|
||||||
|
},
|
||||||
|
new("api", "DorkNet API")
|
||||||
|
{
|
||||||
|
UserClaims = RecRoomIdentityClaims.UserClaimTypes,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly Client[] Clients =
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ClientId = ClientId,
|
||||||
|
ClientName = "Rec Room game client",
|
||||||
|
RequireClientSecret = true,
|
||||||
|
ClientSecrets = { new Secret(ClientSecret.Sha256()) },
|
||||||
|
AllowedGrantTypes = { GrantType.ResourceOwnerPassword, "refresh_token", CachedLoginGrantType },
|
||||||
|
AllowedScopes = RecRoomScopes(),
|
||||||
|
AllowOfflineAccess = true,
|
||||||
|
AccessTokenLifetime = 60 * 60 * 12,
|
||||||
|
AbsoluteRefreshTokenLifetime = 60 * 60 * 24 * 30,
|
||||||
|
SlidingRefreshTokenLifetime = 60 * 60 * 24 * 30,
|
||||||
|
RefreshTokenUsage = TokenUsage.ReUse,
|
||||||
|
RefreshTokenExpiration = TokenExpiration.Absolute,
|
||||||
|
UpdateAccessTokenClaimsOnRefresh = true,
|
||||||
|
AlwaysIncludeUserClaimsInIdToken = true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
private static ICollection<string> RecRoomScopes() =>
|
||||||
|
[
|
||||||
|
IdentityServerConstants.StandardScopes.OpenId,
|
||||||
|
IdentityServerConstants.StandardScopes.Profile,
|
||||||
|
IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||||
|
GameClientScope,
|
||||||
|
"api",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class IdentityServerGameTokenRequestMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
if (!IsTokenRequest(context))
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var form = await context.Request.ReadFormAsync();
|
||||||
|
var values = form.ToDictionary(
|
||||||
|
kvp => kvp.Key,
|
||||||
|
kvp => kvp.Value,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
var changed = false;
|
||||||
|
|
||||||
|
if (IsDevicePasswordGrant(values))
|
||||||
|
{
|
||||||
|
if (!values.ContainsKey("username") || string.IsNullOrWhiteSpace(values["username"].ToString()))
|
||||||
|
{
|
||||||
|
values["username"] = First(
|
||||||
|
values.TryGetValue("deviceId", out var deviceId) ? deviceId.ToString() : null,
|
||||||
|
values.TryGetValue("device_id", out var deviceIdSnake) ? deviceIdSnake.ToString() : null,
|
||||||
|
values.TryGetValue("platformId", out var platformId) ? platformId.ToString() : null,
|
||||||
|
values.TryGetValue("platform_id", out var platformIdSnake) ? platformIdSnake.ToString() : null,
|
||||||
|
"anonymous");
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.ContainsKey("password") || string.IsNullOrWhiteSpace(values["password"].ToString()))
|
||||||
|
{
|
||||||
|
values["password"] = "__dorknet_device_login__";
|
||||||
|
values["dorknet_device_login"] = "true";
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopes = values.TryGetValue("scope", out var rawScope)
|
||||||
|
? rawScope.ToString()
|
||||||
|
: string.Empty;
|
||||||
|
var mergedScopes = MergeScopes(scopes);
|
||||||
|
if (!string.Equals(scopes, mergedScopes, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
values["scope"] = mergedScopes;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
context.Features.Set<IFormFeature>(new FormFeature(new FormCollection(values)));
|
||||||
|
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsTokenRequest(HttpContext context) =>
|
||||||
|
HttpMethods.IsPost(context.Request.Method)
|
||||||
|
&& context.Request.Path.Equals("/connect/token", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& context.Request.HasFormContentType;
|
||||||
|
|
||||||
|
private static bool IsDevicePasswordGrant(Dictionary<string, StringValues> values)
|
||||||
|
{
|
||||||
|
if (!values.TryGetValue("grant_type", out var grantType) ||
|
||||||
|
!string.Equals(grantType.ToString(), GrantType.ResourceOwnerPassword, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
return !values.TryGetValue("password", out var password) ||
|
||||||
|
string.IsNullOrWhiteSpace(password.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string First(params string?[] values) =>
|
||||||
|
values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)) ?? "anonymous";
|
||||||
|
|
||||||
|
private static string MergeScopes(string existing)
|
||||||
|
{
|
||||||
|
var scopes = existing
|
||||||
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
scopes.Add(IdentityServerConstants.StandardScopes.OpenId);
|
||||||
|
scopes.Add(IdentityServerConstants.StandardScopes.Profile);
|
||||||
|
scopes.Add(IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||||
|
scopes.Add(RecRoomIdentityServerConfig.GameClientScope);
|
||||||
|
return string.Join(' ', scopes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RecRoomPasswordValidator(
|
||||||
|
PlayerService playerService,
|
||||||
|
LevelService level,
|
||||||
|
ServerSettingsService settings,
|
||||||
|
DorkNetDbContext db,
|
||||||
|
ILogger<RecRoomPasswordValidator> logger) : IResourceOwnerPasswordValidator
|
||||||
|
{
|
||||||
|
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
|
||||||
|
{
|
||||||
|
var raw = context.Request.Raw;
|
||||||
|
var deviceId = First(raw.Get("deviceId"), raw.Get("device_id"));
|
||||||
|
var platformId = First(raw.Get("platformId"), raw.Get("platform_id"));
|
||||||
|
var platform = int.TryParse(raw.Get("platform"), out var p) ? p : 0;
|
||||||
|
var deviceLogin = string.Equals(raw.Get("dorknet_device_login"), "true", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (!deviceLogin && !string.IsNullOrEmpty(context.Password) && !string.IsNullOrEmpty(context.UserName))
|
||||||
|
{
|
||||||
|
var byName = await playerService.GetByUsernameAsync(context.UserName);
|
||||||
|
if (byName is null || byName.PasswordHash is null)
|
||||||
|
{
|
||||||
|
context.Result = new GrantValidationResult(
|
||||||
|
TokenRequestErrors.InvalidGrant,
|
||||||
|
"unknown_user_or_no_password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BCrypt.Net.BCrypt.Verify(context.Password, byName.PasswordHash))
|
||||||
|
{
|
||||||
|
context.Result = new GrantValidationResult(
|
||||||
|
TokenRequestErrors.InvalidGrant,
|
||||||
|
"password_mismatch");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await playerService.TagPlatformAsync(byName.Id, platform, platformId);
|
||||||
|
await TryAwardLoginXpAsync(byName.Id);
|
||||||
|
context.Result = await CreateSuccessAsync(byName, "password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var effectiveDeviceId = !string.IsNullOrWhiteSpace(deviceId)
|
||||||
|
? deviceId
|
||||||
|
: $"legacy-{(context.UserName ?? "anonymous").ToLowerInvariant()}";
|
||||||
|
|
||||||
|
if (await settings.AreSignupsDisabledAsync())
|
||||||
|
{
|
||||||
|
var existing = await playerService.GetByDeviceAsync(effectiveDeviceId);
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"[auth] IdentityServer password grant device fallback refused - signups disabled (device={Device})",
|
||||||
|
effectiveDeviceId);
|
||||||
|
context.Result = new GrantValidationResult(
|
||||||
|
TokenRequestErrors.InvalidGrant,
|
||||||
|
"signups_disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var player = await playerService.GetOrCreateByDeviceAsync(
|
||||||
|
deviceId: effectiveDeviceId,
|
||||||
|
platform: platform,
|
||||||
|
platformId: platformId,
|
||||||
|
displayName: context.UserName);
|
||||||
|
await TryAwardLoginXpAsync(player.Id);
|
||||||
|
context.Result = await CreateSuccessAsync(player, "device_password");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<GrantValidationResult> CreateSuccessAsync(PlayerEntity player, string authMethod) =>
|
||||||
|
new(
|
||||||
|
subject: player.Id.ToString(),
|
||||||
|
authenticationMethod: authMethod,
|
||||||
|
claims: await RecRoomIdentityClaims.CreateAsync(db, player.Id, player.Username));
|
||||||
|
|
||||||
|
private async Task TryAwardLoginXpAsync(long playerId)
|
||||||
|
{
|
||||||
|
var lastSeen = await db.Players
|
||||||
|
.Where(p => p.Id == playerId)
|
||||||
|
.Select(p => p.LastSeenAt)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (lastSeen.Date < DateTime.UtcNow.Date)
|
||||||
|
await level.AwardXpAsync(playerId, LevelService.FirstLoginXp, "first_login_today");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? First(params string?[] values) =>
|
||||||
|
values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CachedLoginGrantValidator(
|
||||||
|
PlayerService playerService,
|
||||||
|
LevelService level,
|
||||||
|
OrphanAccountTracker orphans,
|
||||||
|
DorkNetDbContext db,
|
||||||
|
ILogger<CachedLoginGrantValidator> logger) : IExtensionGrantValidator
|
||||||
|
{
|
||||||
|
public string GrantType => RecRoomIdentityServerConfig.CachedLoginGrantType;
|
||||||
|
|
||||||
|
public async Task ValidateAsync(ExtensionGrantValidationContext context)
|
||||||
|
{
|
||||||
|
var raw = context.Request.Raw;
|
||||||
|
if (!long.TryParse(raw.Get("account_id"), out var pickedId))
|
||||||
|
{
|
||||||
|
context.Result = new GrantValidationResult(
|
||||||
|
TokenRequestErrors.InvalidGrant,
|
||||||
|
"missing account_id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var picked = await playerService.GetByIdAsync(pickedId);
|
||||||
|
if (picked is null)
|
||||||
|
{
|
||||||
|
context.Result = new GrantValidationResult(
|
||||||
|
TokenRequestErrors.InvalidGrant,
|
||||||
|
"unknown account_id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deviceId = First(raw.Get("deviceId"), raw.Get("device_id"));
|
||||||
|
var platformId = First(raw.Get("platformId"), raw.Get("platform_id"));
|
||||||
|
var platform = int.TryParse(raw.Get("platform"), out var p) ? p : 0;
|
||||||
|
|
||||||
|
var pendingId = orphans.PeekPending(deviceId, platformId);
|
||||||
|
if (pendingId is long orphanId && orphanId != pickedId)
|
||||||
|
{
|
||||||
|
await playerService.DeleteOrphanAsync(orphanId);
|
||||||
|
logger.LogInformation(
|
||||||
|
"[orphan-cleanup] cached_login picked {PickedId}; deleted just-created {OrphanId}",
|
||||||
|
pickedId,
|
||||||
|
orphanId);
|
||||||
|
}
|
||||||
|
orphans.Clear(deviceId, platformId);
|
||||||
|
|
||||||
|
await TryAwardLoginXpAsync(picked.Id);
|
||||||
|
await playerService.TagPlatformAsync(picked.Id, platform, platformId);
|
||||||
|
context.Result = new GrantValidationResult(
|
||||||
|
subject: picked.Id.ToString(),
|
||||||
|
authenticationMethod: GrantType,
|
||||||
|
claims: await RecRoomIdentityClaims.CreateAsync(db, picked.Id, picked.Username));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TryAwardLoginXpAsync(long playerId)
|
||||||
|
{
|
||||||
|
var lastSeen = await db.Players
|
||||||
|
.Where(p => p.Id == playerId)
|
||||||
|
.Select(p => p.LastSeenAt)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (lastSeen.Date < DateTime.UtcNow.Date)
|
||||||
|
await level.AwardXpAsync(playerId, LevelService.FirstLoginXp, "first_login_today");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? First(params string?[] values) =>
|
||||||
|
values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RecRoomProfileService(PlayerService playerService, DorkNetDbContext db) : IProfileService
|
||||||
|
{
|
||||||
|
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
|
||||||
|
{
|
||||||
|
if (!TryGetSubjectId(context.Subject, out var playerId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var player = await playerService.GetByIdAsync(playerId);
|
||||||
|
if (player is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
context.IssuedClaims.AddRange(
|
||||||
|
await RecRoomIdentityClaims.CreateAsync(db, player.Id, player.Username));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task IsActiveAsync(IsActiveContext context)
|
||||||
|
{
|
||||||
|
context.IsActive =
|
||||||
|
TryGetSubjectId(context.Subject, out var playerId)
|
||||||
|
&& await playerService.GetByIdAsync(playerId) is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetSubjectId(ClaimsPrincipal subject, out long playerId)
|
||||||
|
{
|
||||||
|
var value = subject.FindFirstValue(JwtRegisteredClaimNames.Sub)
|
||||||
|
?? subject.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return long.TryParse(value, out playerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RecRoomIdentityClaims
|
||||||
|
{
|
||||||
|
public static readonly List<string> UserClaimTypes =
|
||||||
|
[
|
||||||
|
JwtRegisteredClaimNames.Sub,
|
||||||
|
ClaimTypes.NameIdentifier,
|
||||||
|
"name",
|
||||||
|
"role",
|
||||||
|
ClaimTypes.Role,
|
||||||
|
"roles",
|
||||||
|
"accountId",
|
||||||
|
"account_id",
|
||||||
|
"accountid",
|
||||||
|
];
|
||||||
|
|
||||||
|
public static async Task<List<Claim>> CreateAsync(DorkNetDbContext db, long playerId, string? username = null)
|
||||||
|
{
|
||||||
|
var playerInfo = await db.Players
|
||||||
|
.Where(p => p.Id == playerId)
|
||||||
|
.Select(p => new { p.Username, IsDev = p.IsDeveloper || p.IsAdmin })
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
var name = username ?? playerInfo?.Username ?? playerId.ToString();
|
||||||
|
var isDev = playerInfo?.IsDev ?? false;
|
||||||
|
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(ClaimTypes.NameIdentifier, playerId.ToString()),
|
||||||
|
new(JwtRegisteredClaimNames.Sub, playerId.ToString()),
|
||||||
|
new("name", name),
|
||||||
|
new("accountId", playerId.ToString()),
|
||||||
|
new("account_id", playerId.ToString()),
|
||||||
|
new("accountid", playerId.ToString()),
|
||||||
|
new(ClaimTypes.Role, "gameClient"),
|
||||||
|
new("role", "gameClient"),
|
||||||
|
new("roles", "gameClient"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDev)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(ClaimTypes.Role, "developer"));
|
||||||
|
claims.Add(new Claim("role", "developer"));
|
||||||
|
claims.Add(new Claim("roles", "developer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -201,12 +201,12 @@ public class AccountApiController(
|
|||||||
|
|
||||||
[HttpPost("api/account/v1/profileimage")]
|
[HttpPost("api/account/v1/profileimage")]
|
||||||
[HttpPost("api/account/me/profileimage")]
|
[HttpPost("api/account/me/profileimage")]
|
||||||
[HttpPost("account/me/profileimage")]
|
[HttpPost("account/me/profileimage", Order = 10)]
|
||||||
[HttpPost("profileimage")]
|
[HttpPost("profileimage", Order = 10)]
|
||||||
[HttpPut("api/account/v1/profileimage")]
|
[HttpPut("api/account/v1/profileimage")]
|
||||||
[HttpPut("api/account/me/profileimage")]
|
[HttpPut("api/account/me/profileimage")]
|
||||||
[HttpPut("account/me/profileimage")]
|
[HttpPut("account/me/profileimage", Order = 10)]
|
||||||
[HttpPut("profileimage")]
|
[HttpPut("profileimage", Order = 10)]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
public async Task<IActionResult> SetProfileImage(
|
public async Task<IActionResult> SetProfileImage(
|
||||||
[FromForm(Name = "imageName")] string? imageName,
|
[FromForm(Name = "imageName")] string? imageName,
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ public class ServiceUrlsController(DomainConfig domain) : ControllerBase
|
|||||||
{
|
{
|
||||||
[HttpGet("services")]
|
[HttpGet("services")]
|
||||||
public ActionResult<Dictionary<string, string>> Get()
|
public ActionResult<Dictionary<string, string>> Get()
|
||||||
=> Ok(ConfigService.BuildServiceUrlMap(domain.Apex));
|
=> Ok(ConfigService.BuildServiceUrlMap(domain));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class NotificationController(DomainConfig domain) : ControllerBase
|
|||||||
public IActionResult Connect() => StatusCode(StatusCodes.Status410Gone, new
|
public IActionResult Connect() => StatusCode(StatusCodes.Status410Gone, new
|
||||||
{
|
{
|
||||||
error = "use_signalr_hub",
|
error = "use_signalr_hub",
|
||||||
hint = $"connect to {domain.Sub("notify")}/hub/v1 instead",
|
hint = $"connect to {domain.Url("notify", "/hub/v1")} instead",
|
||||||
});
|
});
|
||||||
|
|
||||||
/// <summary>GET <c>api/notification/hub/v1</c> — returns the
|
/// <summary>GET <c>api/notification/hub/v1</c> — returns the
|
||||||
@@ -27,7 +27,7 @@ public class NotificationController(DomainConfig domain) : ControllerBase
|
|||||||
[HttpGet("/api/notification/hub/v1")]
|
[HttpGet("/api/notification/hub/v1")]
|
||||||
public IActionResult HubInfo() => Ok(new
|
public IActionResult HubInfo() => Ok(new
|
||||||
{
|
{
|
||||||
Url = $"https://{domain.Sub("notify")}/hub/v1",
|
Url = domain.Url("notify", "/hub/v1"),
|
||||||
Protocol = "signalr",
|
Protocol = "signalr",
|
||||||
Version = 1,
|
Version = 1,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ public class PageviewController(ILogger<PageviewController> logger) : Controller
|
|||||||
/// intentionally return an empty url + a 1-hour freshness — the
|
/// intentionally return an empty url + a 1-hour freshness — the
|
||||||
/// private server has no in-app deep-link system, so there's
|
/// private server has no in-app deep-link system, so there's
|
||||||
/// nothing to navigate to and no reason to be polled more often.</summary>
|
/// nothing to navigate to and no reason to be polled more often.</summary>
|
||||||
[HttpPost("pageview/consume")]
|
[HttpPost("pageview/consume", Order = 10)]
|
||||||
[HttpPost("/pageview/consume")]
|
[HttpPost("/pageview/consume")]
|
||||||
[HttpGet("pageview/consume")]
|
[HttpGet("pageview/consume", Order = 10)]
|
||||||
[HttpGet("/pageview/consume")]
|
[HttpGet("/pageview/consume")]
|
||||||
public IActionResult Consume()
|
public IActionResult Consume()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using DorkNet.Models.Notification;
|
using DorkNet.Models.Notification;
|
||||||
using DorkNet.Server.Auth;
|
using DorkNet.Server.Auth;
|
||||||
using DorkNet.Server.Data;
|
using DorkNet.Server.Data;
|
||||||
@@ -39,6 +40,7 @@ public class AdminController(
|
|||||||
SignupCodeService signupCodes,
|
SignupCodeService signupCodes,
|
||||||
IObjectStorage storage,
|
IObjectStorage storage,
|
||||||
DomainConfig domain,
|
DomainConfig domain,
|
||||||
|
IdentityServerSigningKeyProvider signingKeys,
|
||||||
ILogger<AdminController> adminLogger) : ControllerBase
|
ILogger<AdminController> adminLogger) : ControllerBase
|
||||||
{
|
{
|
||||||
private long CurrentAdminId => this.RequireCurrentPlayerId();
|
private long CurrentAdminId => this.RequireCurrentPlayerId();
|
||||||
@@ -1460,6 +1462,52 @@ public class AdminController(
|
|||||||
|
|
||||||
// ── Server settings (runtime toggles) ────────────────────────────────
|
// ── Server settings (runtime toggles) ────────────────────────────────
|
||||||
|
|
||||||
|
[HttpGet("identityserver")]
|
||||||
|
public ActionResult GetIdentityServerSettings()
|
||||||
|
{
|
||||||
|
var issuer = domain.AuthIssuer.TrimEnd('/');
|
||||||
|
var client = RecRoomIdentityServerConfig.Clients[0];
|
||||||
|
var certPath = signingKeys.CertificatePath;
|
||||||
|
var fullCertPath = Path.GetFullPath(certPath);
|
||||||
|
var certificateExists = signingKeys.LoadedFromBase64 || System.IO.File.Exists(fullCertPath);
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
Issuer = issuer,
|
||||||
|
DiscoveryUrl = $"{issuer}/.well-known/openid-configuration",
|
||||||
|
TokenEndpoint = $"{issuer}/connect/token",
|
||||||
|
UserInfoEndpoint = $"{issuer}/connect/userinfo",
|
||||||
|
Client = new
|
||||||
|
{
|
||||||
|
client.ClientId,
|
||||||
|
client.ClientName,
|
||||||
|
AllowedGrantTypes = client.AllowedGrantTypes.ToArray(),
|
||||||
|
AllowedScopes = client.AllowedScopes.ToArray(),
|
||||||
|
client.AllowOfflineAccess,
|
||||||
|
client.AccessTokenLifetime,
|
||||||
|
client.AbsoluteRefreshTokenLifetime,
|
||||||
|
client.SlidingRefreshTokenLifetime,
|
||||||
|
RefreshTokenUsage = client.RefreshTokenUsage.ToString(),
|
||||||
|
RefreshTokenExpiration = client.RefreshTokenExpiration.ToString(),
|
||||||
|
},
|
||||||
|
Signing = new
|
||||||
|
{
|
||||||
|
signingKeys.CertificateSource,
|
||||||
|
signingKeys.LoadedFromBase64,
|
||||||
|
CertificatePath = fullCertPath,
|
||||||
|
CertificateExists = certificateExists,
|
||||||
|
signingKeys.Certificate.Thumbprint,
|
||||||
|
NotBeforeUtc = signingKeys.Certificate.NotBefore.ToUniversalTime(),
|
||||||
|
NotAfterUtc = signingKeys.Certificate.NotAfter.ToUniversalTime(),
|
||||||
|
Algorithm = SecurityAlgorithms.RsaSha256,
|
||||||
|
},
|
||||||
|
PersistedGrants = new
|
||||||
|
{
|
||||||
|
Store = "in-memory",
|
||||||
|
Warning = "Refresh tokens are process-local until an operational store is configured.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
/// <summary>GET <c>api/admin/v1/settings</c> — current server-wide
|
/// <summary>GET <c>api/admin/v1/settings</c> — current server-wide
|
||||||
/// toggle state. Single row, single round trip.</summary>
|
/// toggle state. Single row, single round trip.</summary>
|
||||||
[HttpGet("settings")]
|
[HttpGet("settings")]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class AuthAccountController(DorkNetDbContext db) : ControllerBase
|
|||||||
/// <c>ExpectPrimitiveResponse<bool></c> so the body must be
|
/// <c>ExpectPrimitiveResponse<bool></c> so the body must be
|
||||||
/// the literal JSON <c>true</c> or <c>false</c>.</summary>
|
/// the literal JSON <c>true</c> or <c>false</c>.</summary>
|
||||||
[HttpGet("/account/me/haspassword")]
|
[HttpGet("/account/me/haspassword")]
|
||||||
[HttpGet("/api/platformlogin/haspassword")]
|
[HttpGet("/api/platformlogin/haspassword", Order = 10)]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> HasPassword()
|
public async Task<IActionResult> HasPassword()
|
||||||
{
|
{
|
||||||
@@ -40,8 +40,8 @@ public class AuthAccountController(DorkNetDbContext db) : ControllerBase
|
|||||||
/// one; first-time creation skips that check. Form-encoded body.</summary>
|
/// one; first-time creation skips that check. Form-encoded body.</summary>
|
||||||
[HttpPost("/account/me/changepassword")]
|
[HttpPost("/account/me/changepassword")]
|
||||||
[HttpPost("/account/me/createpassword")]
|
[HttpPost("/account/me/createpassword")]
|
||||||
[HttpPost("/api/platformlogin/password")]
|
[HttpPost("/api/platformlogin/password", Order = 10)]
|
||||||
[HttpPut("/api/platformlogin/password")]
|
[HttpPut("/api/platformlogin/password", Order = 10)]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> ChangePassword(
|
public async Task<IActionResult> ChangePassword(
|
||||||
@@ -76,7 +76,7 @@ public class AuthAccountController(DorkNetDbContext db) : ControllerBase
|
|||||||
/// account from the admin tab and the user re-creates it via
|
/// account from the admin tab and the user re-creates it via
|
||||||
/// changepassword on next login. Anonymous calls 401.</summary>
|
/// changepassword on next login. Anonymous calls 401.</summary>
|
||||||
[HttpPost("/account/me/resetpassword")]
|
[HttpPost("/account/me/resetpassword")]
|
||||||
[HttpPost("/api/platformlogin/resetpassword")]
|
[HttpPost("/api/platformlogin/resetpassword", Order = 10)]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[AdminOnly]
|
[AdminOnly]
|
||||||
public async Task<IActionResult> AdminResetPassword(
|
public async Task<IActionResult> AdminResetPassword(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using DorkNet.Models.Auth;
|
using DorkNet.Models.Auth;
|
||||||
using DorkNet.Server.Data.Entities;
|
using DorkNet.Server.Data.Entities;
|
||||||
using DorkNet.Server.Services;
|
using DorkNet.Server.Services;
|
||||||
@@ -7,241 +6,18 @@ using DorkNet.Server.Services;
|
|||||||
namespace DorkNet.Server.Controllers.Auth;
|
namespace DorkNet.Server.Controllers.Auth;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// auth.rec.net — OAuth-style token endpoint used by 2019+ builds alongside PlatformLogin.
|
/// auth.rec.net game-specific auth helpers. OAuth/OIDC endpoints are owned by IdentityServer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class AuthController(
|
public class AuthController(
|
||||||
PlayerService playerService,
|
PlayerService playerService,
|
||||||
AuthService authService,
|
AuthService authService) : ControllerBase
|
||||||
LevelService level,
|
|
||||||
OrphanAccountTracker orphans,
|
|
||||||
ServerSettingsService settings,
|
|
||||||
DorkNet.Server.Data.DorkNetDbContext db,
|
|
||||||
DomainConfig domain,
|
|
||||||
ILogger<AuthController> logger) : ControllerBase
|
|
||||||
{
|
{
|
||||||
/// <summary>Award the per-day first-login XP if the calling
|
|
||||||
/// player hasn't been seen yet today (UTC).</summary>
|
|
||||||
private async Task TryAwardLoginXpAsync(long playerId)
|
|
||||||
{
|
|
||||||
var lastSeen = await db.Players
|
|
||||||
.Where(p => p.Id == playerId)
|
|
||||||
.Select(p => p.LastSeenAt)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (lastSeen.Date < DateTime.UtcNow.Date)
|
|
||||||
await level.AwardXpAsync(playerId, LevelService.FirstLoginXp, "first_login_today");
|
|
||||||
}
|
|
||||||
// OAuth token endpoint. The 2020 client uses three grant types here:
|
|
||||||
// - grant_type=cached_login → user clicked a remembered account on the
|
|
||||||
// account-selection screen. Body includes `account_id` (and platform
|
|
||||||
// creds). MUST issue a token for THAT specific account; otherwise the
|
|
||||||
// client receives a JWT for a different account and prompts the user
|
|
||||||
// to create a new one (because the JWT identity doesn't match what
|
|
||||||
// they picked).
|
|
||||||
// - grant_type=refresh_token → re-issue access token from the existing
|
|
||||||
// refresh JWT.
|
|
||||||
// - grant_type=password → first-time sign-in, resolve via deviceId
|
|
||||||
// (preferred) or fall back to a stable synthetic id derived from
|
|
||||||
// username so legacy callers don't share accounts.
|
|
||||||
[HttpPost("/connect/token")]
|
|
||||||
[Consumes("application/x-www-form-urlencoded")]
|
|
||||||
public async Task<IActionResult> Token(
|
|
||||||
[FromForm] string? grant_type,
|
|
||||||
[FromForm] string? username,
|
|
||||||
[FromForm] string? password,
|
|
||||||
[FromForm] string? refresh_token,
|
|
||||||
[FromForm] string? deviceId,
|
|
||||||
[FromForm(Name = "device_id")] string? device_id_snake,
|
|
||||||
[FromForm] int platform = 0,
|
|
||||||
[FromForm] string? platformId = null,
|
|
||||||
[FromForm(Name = "platform_id")] string? platform_id_snake = null,
|
|
||||||
[FromForm] long? account_id = null)
|
|
||||||
{
|
|
||||||
// The client mixes camelCase and snake_case parameter naming
|
|
||||||
// depending on grant type — accept both and merge.
|
|
||||||
var effDeviceId = !string.IsNullOrWhiteSpace(deviceId) ? deviceId : device_id_snake;
|
|
||||||
var effPlatformId = !string.IsNullOrWhiteSpace(platformId) ? platformId : platform_id_snake;
|
|
||||||
|
|
||||||
if (string.Equals(grant_type, "cached_login", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (account_id is not long pickedId)
|
|
||||||
return BadRequest(new { error = "invalid_request", error_description = "missing account_id" });
|
|
||||||
var picked = await playerService.GetByIdAsync(pickedId);
|
|
||||||
if (picked is null)
|
|
||||||
return Unauthorized(new { error = "invalid_grant", error_description = "unknown account_id" });
|
|
||||||
|
|
||||||
// Orphan cleanup: if the watch just created a brand-new
|
|
||||||
// account on account/create as part of its boot probe, but
|
|
||||||
// the user actually picked a different cached account here,
|
|
||||||
// the just-created row is unused. Delete it so the DB
|
|
||||||
// doesn't accumulate one orphan per game launch. When the
|
|
||||||
// user is genuinely creating a new account, the picked id
|
|
||||||
// matches the just-created id and we leave it.
|
|
||||||
var pendingId = orphans.PeekPending(effDeviceId, effPlatformId);
|
|
||||||
if (pendingId is long orphanId && orphanId != pickedId)
|
|
||||||
{
|
|
||||||
await playerService.DeleteOrphanAsync(orphanId);
|
|
||||||
logger.LogInformation(
|
|
||||||
"[orphan-cleanup] cached_login picked {PickedId}; deleted just-created {OrphanId}",
|
|
||||||
pickedId, orphanId);
|
|
||||||
}
|
|
||||||
orphans.Clear(effDeviceId, effPlatformId);
|
|
||||||
|
|
||||||
await TryAwardLoginXpAsync(picked.Id);
|
|
||||||
await playerService.TagPlatformAsync(picked.Id, platform, effPlatformId);
|
|
||||||
var (acc1, refr1) = authService.GenerateTokenPair(picked.Id);
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
access_token = acc1,
|
|
||||||
token_type = "Bearer",
|
|
||||||
expires_in = 43200,
|
|
||||||
refresh_token = refr1,
|
|
||||||
scope = "openid profile",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(grant_type, "refresh_token", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(refresh_token))
|
|
||||||
return BadRequest(new { error = "invalid_request" });
|
|
||||||
var resolved = authService.ValidateToken(refresh_token);
|
|
||||||
if (resolved is not long refreshedId)
|
|
||||||
return Unauthorized(new { error = "invalid_grant" });
|
|
||||||
var existing = await playerService.GetByIdAsync(refreshedId);
|
|
||||||
if (existing is null)
|
|
||||||
return Unauthorized(new { error = "invalid_grant" });
|
|
||||||
var (acc, refr) = authService.GenerateTokenPair(existing.Id);
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
access_token = acc,
|
|
||||||
token_type = "Bearer",
|
|
||||||
expires_in = 43200,
|
|
||||||
refresh_token = refr,
|
|
||||||
scope = "openid profile",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// password grant: two paths.
|
|
||||||
// • Username + password supplied: real password verify against
|
|
||||||
// PlayerEntity.PasswordHash. Wrong password → 401.
|
|
||||||
// • No password: legacy device-id synth path for the 2020
|
|
||||||
// client's first-launch flow where the player hasn't set a
|
|
||||||
// password yet (HasPassword returns false in the watch UI
|
|
||||||
// and prompts them to create one). Resolves the existing
|
|
||||||
// account (or creates a fresh one) by deviceId.
|
|
||||||
if (!string.IsNullOrEmpty(password) && !string.IsNullOrEmpty(username))
|
|
||||||
{
|
|
||||||
var byName = await playerService.GetByUsernameAsync(username);
|
|
||||||
if (byName is null || byName.PasswordHash is null)
|
|
||||||
return Unauthorized(new
|
|
||||||
{
|
|
||||||
error = "invalid_grant",
|
|
||||||
error_description = "unknown_user_or_no_password",
|
|
||||||
});
|
|
||||||
if (!BCrypt.Net.BCrypt.Verify(password, byName.PasswordHash))
|
|
||||||
return Unauthorized(new
|
|
||||||
{
|
|
||||||
error = "invalid_grant",
|
|
||||||
error_description = "password_mismatch",
|
|
||||||
});
|
|
||||||
// Tag the platform so the next boot's cached-login lookup
|
|
||||||
// finds this account — without this the watch's "remembered
|
|
||||||
// accounts" prompt would stay empty after a manual sign-in.
|
|
||||||
await playerService.TagPlatformAsync(byName.Id, platform, effPlatformId);
|
|
||||||
await TryAwardLoginXpAsync(byName.Id);
|
|
||||||
var pair = authService.GenerateTokenPair(byName.Id);
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
access_token = pair.AccessToken,
|
|
||||||
token_type = "Bearer",
|
|
||||||
expires_in = 43200,
|
|
||||||
refresh_token = pair.RefreshToken,
|
|
||||||
scope = "openid profile",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device-id fallback path. Prefer the real deviceId; fall back
|
|
||||||
// to a stable legacy synthesis from username so older callers
|
|
||||||
// don't accidentally share an account.
|
|
||||||
var effectiveDeviceId = !string.IsNullOrWhiteSpace(effDeviceId)
|
|
||||||
? effDeviceId
|
|
||||||
: $"legacy-{(username ?? "anonymous").ToLowerInvariant()}";
|
|
||||||
|
|
||||||
// Honour the admin "signups disabled" toggle. If the device
|
|
||||||
// hasn't logged in before, GetOrCreateByDeviceAsync would mint
|
|
||||||
// a fresh account here — same outcome as hitting
|
|
||||||
// /account/create directly, which the toggle blocks. Refuse
|
|
||||||
// BEFORE creation so the flag isn't bypassable.
|
|
||||||
if (await settings.AreSignupsDisabledAsync())
|
|
||||||
{
|
|
||||||
var existing = await playerService.GetByDeviceAsync(effectiveDeviceId);
|
|
||||||
if (existing is null)
|
|
||||||
{
|
|
||||||
logger.LogInformation(
|
|
||||||
"[auth] /connect/token device fallback refused — signups disabled (device={Device})",
|
|
||||||
effectiveDeviceId);
|
|
||||||
return Unauthorized(new
|
|
||||||
{
|
|
||||||
error = "signups_disabled",
|
|
||||||
error_description = "Account creation is currently disabled by the server admin.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var player = await playerService.GetOrCreateByDeviceAsync(
|
|
||||||
deviceId: effectiveDeviceId,
|
|
||||||
platform: platform,
|
|
||||||
platformId: effPlatformId,
|
|
||||||
displayName: username);
|
|
||||||
await TryAwardLoginXpAsync(player.Id);
|
|
||||||
var (access, refresh) = authService.GenerateTokenPair(player.Id);
|
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
access_token = access,
|
|
||||||
token_type = "Bearer",
|
|
||||||
expires_in = 43200,
|
|
||||||
refresh_token = refresh,
|
|
||||||
scope = "openid profile",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("/connect/userinfo")]
|
|
||||||
public async Task<IActionResult> UserInfo()
|
|
||||||
{
|
|
||||||
var auth = Request.Headers.Authorization.ToString();
|
|
||||||
if (!auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return Unauthorized();
|
|
||||||
var token = auth["Bearer ".Length..].Trim();
|
|
||||||
var id = authService.ValidateToken(token);
|
|
||||||
if (id is not long playerId) return Unauthorized();
|
|
||||||
var player = await playerService.GetByIdAsync(playerId);
|
|
||||||
if (player is null) return Unauthorized();
|
|
||||||
return Ok(new { sub = player.Id.ToString(), name = player.Username });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discovery document — issuer + endpoints derived from
|
|
||||||
// DomainConfig so a single DORKNET_DOMAIN env var keeps the
|
|
||||||
// OpenID document pointed at the live deployment.
|
|
||||||
[HttpGet("/.well-known/openid-configuration")]
|
|
||||||
public IActionResult OpenIdConfig()
|
|
||||||
{
|
|
||||||
var base_ = $"https://{domain.Sub("auth")}";
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
issuer = base_,
|
|
||||||
token_endpoint = $"{base_}/connect/token",
|
|
||||||
userinfo_endpoint = $"{base_}/connect/userinfo",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2020.12 added an EAC challenge step in the login flow. The client GETs
|
// 2020.12 added an EAC challenge step in the login flow. The client GETs
|
||||||
// a challenge string and passes it to EACManager.GenerateChallengeResponse,
|
// a challenge string and passes it to EACManager.GenerateChallengeResponse,
|
||||||
// which calls System.Convert.FromBase64CharPtr on the string. So the
|
// which calls System.Convert.FromBase64CharPtr on the string. So the
|
||||||
// returned challenge MUST be valid base64 — sending arbitrary text like
|
// returned challenge MUST be valid base64 — sending arbitrary text throws
|
||||||
// "dorknet-eac-disabled" throws FormatException client-side and kicks
|
// FormatException client-side and kicks the login.
|
||||||
// the login. 24 chars of base64 'A's decodes to 18 zero bytes; matches
|
|
||||||
// the rough size of a real EAC handshake nonce.
|
|
||||||
[HttpGet("/eac/challenge")]
|
[HttpGet("/eac/challenge")]
|
||||||
[HttpPost("/eac/challenge")]
|
[HttpPost("/eac/challenge")]
|
||||||
public IActionResult EacChallenge()
|
public IActionResult EacChallenge()
|
||||||
@@ -250,11 +26,6 @@ public class AuthController(
|
|||||||
// Cached-logins lookup — boot-sequence call from ScreenAccountSelectionManager.
|
// Cached-logins lookup — boot-sequence call from ScreenAccountSelectionManager.
|
||||||
// URL: GET auth.rec.net/cachedlogin/forplatformid/{platform}/{platformId}
|
// URL: GET auth.rec.net/cachedlogin/forplatformid/{platform}/{platformId}
|
||||||
// Returns List<Login.CachedLogin>. Empty array = no remembered accounts.
|
// Returns List<Login.CachedLogin>. Empty array = no remembered accounts.
|
||||||
//
|
|
||||||
// Real lookup: any account whose last platformlogin matched
|
|
||||||
// (platform, platformId) shows up here, sorted most-recent-first. On a
|
|
||||||
// single-Steam-emu setup that's one row; on a multi-account dev box
|
|
||||||
// it's the full list the user has cycled through.
|
|
||||||
[HttpGet("/cachedlogin/forplatformid/{platform:int}/{platformId}")]
|
[HttpGet("/cachedlogin/forplatformid/{platform:int}/{platformId}")]
|
||||||
public async Task<IActionResult> CachedLoginsForPlatform(int platform, string platformId)
|
public async Task<IActionResult> CachedLoginsForPlatform(int platform, string platformId)
|
||||||
{
|
{
|
||||||
@@ -264,10 +35,8 @@ public class AuthController(
|
|||||||
|
|
||||||
/// <summary>POST <c>cachedlogin/forplatformids</c> — bulk variant
|
/// <summary>POST <c>cachedlogin/forplatformids</c> — bulk variant
|
||||||
/// the watch uses when hydrating multiple platform-id mappings
|
/// the watch uses when hydrating multiple platform-id mappings
|
||||||
/// at once (e.g. populating a friend list from Steam ids). Body
|
/// at once. Body is form-urlencoded <c>platform=&platformIds=a,b,c</c>
|
||||||
/// is form-urlencoded <c>platform=&platformIds=a,b,c</c> or
|
/// or JSON with the same shape.</summary>
|
||||||
/// JSON with the same shape. Returns the flattened
|
|
||||||
/// <c>List<CachedLogin></c> across every matched id.</summary>
|
|
||||||
[HttpPost("/cachedlogin/forplatformids")]
|
[HttpPost("/cachedlogin/forplatformids")]
|
||||||
[HttpGet("/cachedlogin/forplatformids")]
|
[HttpGet("/cachedlogin/forplatformids")]
|
||||||
public async Task<IActionResult> CachedLoginsForPlatformIds()
|
public async Task<IActionResult> CachedLoginsForPlatformIds()
|
||||||
@@ -285,6 +54,7 @@ public class AuthController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { /* not form */ }
|
catch { /* not form */ }
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(rawIds))
|
if (string.IsNullOrWhiteSpace(rawIds))
|
||||||
{
|
{
|
||||||
int.TryParse(Request.Query["platform"], out platform);
|
int.TryParse(Request.Query["platform"], out platform);
|
||||||
@@ -307,13 +77,50 @@ public class AuthController(
|
|||||||
return Ok(results);
|
return Ok(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CachedLogin ToCachedLogin(PlayerEntity p, int platform, string platformId) => new()
|
[HttpGet("/cachedlogin/current")]
|
||||||
|
public async Task<IActionResult> CurrentCachedLogin()
|
||||||
|
{
|
||||||
|
var token = GetBearerToken();
|
||||||
|
if (token is null) return Unauthorized();
|
||||||
|
var id = authService.ValidateToken(token);
|
||||||
|
if (id is not long playerId) return Unauthorized();
|
||||||
|
var player = await playerService.GetByIdAsync(playerId);
|
||||||
|
if (player is null) return Unauthorized();
|
||||||
|
return Ok(ToCachedLogin(player, player.LastPlatform, player.LastPlatformId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("/cachedlogin/migrate")]
|
||||||
|
public async Task<IActionResult> MigrateCachedLogin(
|
||||||
|
[FromForm] int platform = 0,
|
||||||
|
[FromForm] string? platformId = null)
|
||||||
|
{
|
||||||
|
var token = GetBearerToken();
|
||||||
|
if (token is null) return Unauthorized();
|
||||||
|
var id = authService.ValidateToken(token);
|
||||||
|
if (id is not long playerId) return Unauthorized();
|
||||||
|
await playerService.TagPlatformAsync(playerId, platform, platformId);
|
||||||
|
var player = await playerService.GetByIdAsync(playerId);
|
||||||
|
return player is null
|
||||||
|
? Unauthorized()
|
||||||
|
: Ok(ToCachedLogin(player, platform, platformId ?? string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetBearerToken()
|
||||||
|
{
|
||||||
|
var auth = Request.Headers.Authorization.ToString();
|
||||||
|
if (auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return auth["Bearer ".Length..].Trim();
|
||||||
|
return Request.Cookies.TryGetValue(AuthService.AccessCookieName, out var cookieToken)
|
||||||
|
? cookieToken
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CachedLogin ToCachedLogin(PlayerEntity p, int platform, string? platformId) => new()
|
||||||
{
|
{
|
||||||
Platform = platform,
|
Platform = platform,
|
||||||
PlatformId = platformId,
|
PlatformId = platformId ?? string.Empty,
|
||||||
AccountId = (int)p.Id,
|
AccountId = (int)p.Id,
|
||||||
LastLoginTime = p.LastSeenAt,
|
LastLoginTime = p.LastSeenAt,
|
||||||
RequirePassword = false,
|
RequirePassword = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ public class VersionCheckController(
|
|||||||
private const int VersionStatus_ValidForMenu = 1;
|
private const int VersionStatus_ValidForMenu = 1;
|
||||||
private const int VersionStatus_UpdateRequired = 2;
|
private const int VersionStatus_UpdateRequired = 2;
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> BuiltInBuildIdToVersionKey =
|
||||||
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["20201210"] = "december_2020_12_18",
|
||||||
|
};
|
||||||
|
|
||||||
[HttpGet("/api/versioncheck/v4")]
|
[HttpGet("/api/versioncheck/v4")]
|
||||||
[HttpGet("/api/versioncheck/v3")]
|
[HttpGet("/api/versioncheck/v3")]
|
||||||
[HttpGet("/api/versioncheck/v2")]
|
[HttpGet("/api/versioncheck/v2")]
|
||||||
@@ -66,9 +72,17 @@ public class VersionCheckController(
|
|||||||
// }
|
// }
|
||||||
// Mapping ABSENT → unknown build → UpdateRequired.
|
// Mapping ABSENT → unknown build → UpdateRequired.
|
||||||
var mapSection = config.GetSection("DorkNet:BuildIdToVersionKey");
|
var mapSection = config.GetSection("DorkNet:BuildIdToVersionKey");
|
||||||
string? versionKey = !string.IsNullOrWhiteSpace(buildId)
|
string? versionKey = null;
|
||||||
? mapSection[buildId]
|
if (!string.IsNullOrWhiteSpace(buildId))
|
||||||
: null;
|
{
|
||||||
|
var normalizedBuildId = buildId.Trim();
|
||||||
|
versionKey = mapSection[normalizedBuildId];
|
||||||
|
if (string.IsNullOrWhiteSpace(versionKey)
|
||||||
|
&& BuiltInBuildIdToVersionKey.TryGetValue(normalizedBuildId, out var builtInVersionKey))
|
||||||
|
{
|
||||||
|
versionKey = builtInVersionKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool allowed = versionKey is not null && registry.IsSupported(versionKey);
|
bool allowed = versionKey is not null && registry.IsSupported(versionKey);
|
||||||
var status = allowed ? VersionStatus_ValidForPlay : VersionStatus_UpdateRequired;
|
var status = allowed ? VersionStatus_ValidForPlay : VersionStatus_UpdateRequired;
|
||||||
|
|||||||
@@ -668,11 +668,14 @@ public class ClubsController(ClubService clubs) : ControllerBase
|
|||||||
[HttpPost("/members/bulk")]
|
[HttpPost("/members/bulk")]
|
||||||
public async Task<IActionResult> MembersBulk()
|
public async Task<IActionResult> MembersBulk()
|
||||||
{
|
{
|
||||||
|
var form = Request.HasFormContentType ? Request.Form : null;
|
||||||
if (!long.TryParse(Request.Query["clubId"].ToString(), out var clubId)
|
if (!long.TryParse(Request.Query["clubId"].ToString(), out var clubId)
|
||||||
&& !long.TryParse(Request.Form["clubId"].ToString(), out clubId))
|
&& (form is null || !long.TryParse(form["clubId"].ToString(), out clubId)))
|
||||||
return BadRequest(new { error = "missing_club_id" });
|
return BadRequest(new { error = "missing_club_id" });
|
||||||
|
|
||||||
var idsRaw = Request.Query["playerIds"].Concat(Request.Form["playerIds"]);
|
var idsRaw = form is null
|
||||||
|
? Request.Query["playerIds"].AsEnumerable()
|
||||||
|
: Request.Query["playerIds"].Concat(form["playerIds"]);
|
||||||
var ids = idsRaw
|
var ids = idsRaw
|
||||||
.SelectMany(v => (v ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
.SelectMany(v => (v ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
.Select(s => long.TryParse(s, out var n) ? n : 0)
|
.Select(s => long.TryParse(s, out var n) ? n : 0)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ namespace DorkNet.Server.Controllers.Match;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
public class MatchPlayerController(
|
public class MatchPlayerController(
|
||||||
PlayerPresenceService presence,
|
PlayerPresenceService presence,
|
||||||
NotificationService notifications,
|
|
||||||
RoomService rooms,
|
RoomService rooms,
|
||||||
PrivateInstanceService privateInstances,
|
PrivateInstanceService privateInstances,
|
||||||
IConfiguration config,
|
IConfiguration config,
|
||||||
@@ -37,7 +36,7 @@ public class MatchPlayerController(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("/player/login")]
|
[HttpPost("/player/login")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
|
[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();
|
var playerId = TryGetCurrentPlayerId();
|
||||||
if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock))
|
if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock))
|
||||||
@@ -46,17 +45,15 @@ public class MatchPlayerController(
|
|||||||
if (replaced)
|
if (replaced)
|
||||||
{
|
{
|
||||||
log.LogWarning(
|
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);
|
playerId);
|
||||||
// The new session hasn't opened its SignalR socket yet
|
// Do not broadcast ModerationKick to the whole player group
|
||||||
// (negotiate fires AFTER /player/login on the watch's
|
// here. During reconnects the active watch can already have a
|
||||||
// boot sequence — verified in the live trace), so the
|
// SignalR connection in that group, and the 2020 client crashes
|
||||||
// ModerationKick push hits only the OLDER session's
|
// hard if it receives a malformed/stale moderation payload.
|
||||||
// existing connections. They'll show the "you've been
|
// NotifyHub.OnConnectedAsync snapshots existing connection ids
|
||||||
// kicked" dialog and gracefully disconnect via the
|
// before adding the new connection and sends a targeted kick to
|
||||||
// watch's LocalModerationKickSelf path.
|
// those ids only.
|
||||||
await notifications.KickStaleSession(playerId,
|
|
||||||
"Your account was signed in on another device.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Ok(Array.Empty<object>());
|
return Ok(Array.Empty<object>());
|
||||||
@@ -223,9 +220,12 @@ public class MatchPlayerController(
|
|||||||
var playerId = TryGetCurrentPlayerId();
|
var playerId = TryGetCurrentPlayerId();
|
||||||
if (playerId == 0) return Ok();
|
if (playerId == 0) return Ok();
|
||||||
|
|
||||||
var form = Request.Form;
|
var regions = Request.HasFormContentType
|
||||||
var regions = form["region"].ToArray();
|
? Request.Form["region"].ToArray()
|
||||||
var pings = form["ping"].ToArray();
|
: Request.Query["region"].ToArray();
|
||||||
|
var pings = Request.HasFormContentType
|
||||||
|
? Request.Form["ping"].ToArray()
|
||||||
|
: Request.Query["ping"].ToArray();
|
||||||
var pairs = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
var pairs = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
for (var i = 0; i < Math.Min(regions.Length, pings.Length); i++)
|
for (var i = 0; i < Math.Min(regions.Length, pings.Length); i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ public class NsController : ControllerBase
|
|||||||
// code: locker)" in BootSequence.
|
// code: locker)" in BootSequence.
|
||||||
[HttpGet("/", Order = -1000)]
|
[HttpGet("/", Order = -1000)]
|
||||||
public ActionResult<Dictionary<string, string>> Root() =>
|
public ActionResult<Dictionary<string, string>> Root() =>
|
||||||
Ok(ConfigService.BuildServiceUrlMap(_domain.Apex));
|
Ok(ConfigService.BuildServiceUrlMap(_domain));
|
||||||
|
|
||||||
// Some builds fetch /v1 or /v1/services directly
|
// Some builds fetch /v1 or /v1/services directly
|
||||||
[HttpGet("/v1")]
|
[HttpGet("/v1")]
|
||||||
[HttpGet("/v1/services")]
|
[HttpGet("/v1/services")]
|
||||||
public ActionResult<Dictionary<string, string>> Services() =>
|
public ActionResult<Dictionary<string, string>> Services() =>
|
||||||
Ok(ConfigService.BuildServiceUrlMap(_domain.Apex));
|
Ok(ConfigService.BuildServiceUrlMap(_domain));
|
||||||
|
|
||||||
// Ping / health used by the client keep-alive loop
|
// Ping / health used by the client keep-alive loop
|
||||||
[HttpGet("/v1/ping")]
|
[HttpGet("/v1/ping")]
|
||||||
|
|||||||
@@ -180,8 +180,13 @@ public static class LegacyUpgrades
|
|||||||
// either no-op (Crescendo now exists) or the only writer.
|
// either no-op (Crescendo now exists) or the only writer.
|
||||||
for (var attempt = 1; attempt <= 2; attempt++)
|
for (var attempt = 1; attempt <= 2; attempt++)
|
||||||
{
|
{
|
||||||
var crescendoExists = await db.Rooms.AnyAsync(r => r.Name == "Crescendo", ct);
|
var renameCandidates = await db.Rooms
|
||||||
var bloodMoon = await db.Rooms.FirstOrDefaultAsync(r => r.Name == "BloodMoon", ct);
|
.Where(r => r.Name.ToLower() == "bloodmoon" || r.Name.ToLower() == "crescendo")
|
||||||
|
.ToListAsync(ct);
|
||||||
|
var crescendoExists = renameCandidates.Any(r =>
|
||||||
|
string.Equals(r.Name, "Crescendo", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var bloodMoon = renameCandidates.FirstOrDefault(r =>
|
||||||
|
string.Equals(r.Name, "BloodMoon", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (bloodMoon is null && crescendoExists)
|
if (bloodMoon is null && crescendoExists)
|
||||||
{
|
{
|
||||||
@@ -217,15 +222,15 @@ public static class LegacyUpgrades
|
|||||||
{
|
{
|
||||||
// Both exist — the rename ran on a different row at
|
// Both exist — the rename ran on a different row at
|
||||||
// some point, and the original BloodMoon row is now
|
// some point, and the original BloodMoon row is now
|
||||||
// a stale duplicate. Park it under CrescendoLegacy
|
// a stale duplicate. Park it under a unique legacy
|
||||||
// and hide from browse so /goto/name/... still
|
// name and hide from browse so /goto/name/... still
|
||||||
// resolves Crescendo to the right row.
|
// resolves Crescendo to the right row.
|
||||||
bloodMoon.Name = "CrescendoLegacy";
|
bloodMoon.Name = await PickUniqueRoomNameAsync(db, "CrescendoLegacy", bloodMoon.Id, ct);
|
||||||
bloodMoon.HiddenFromBrowse = true;
|
bloodMoon.HiddenFromBrowse = true;
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"[legacy-upgrade] RenameBloodMoonToCrescendo: applied — Rooms.Id={Id} parked as CrescendoLegacy (Crescendo already existed on a different row)",
|
"[legacy-upgrade] RenameBloodMoonToCrescendo: applied — Rooms.Id={Id} parked as {Name} (Crescendo already existed on a different row)",
|
||||||
bloodMoon.Id);
|
bloodMoon.Id, bloodMoon.Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,4 +246,22 @@ public static class LegacyUpgrades
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<string> PickUniqueRoomNameAsync(
|
||||||
|
DorkNetDbContext db, string preferred, long currentRoomId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var taken = await db.Rooms
|
||||||
|
.Where(r => r.Id != currentRoomId && r.Name.ToLower().StartsWith(preferred.ToLower()))
|
||||||
|
.Select(r => r.Name)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
if (!taken.Any(n => string.Equals(n, preferred, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return preferred;
|
||||||
|
|
||||||
|
var suffix = 2;
|
||||||
|
while (taken.Any(n => string.Equals(n, $"{preferred}{suffix}", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
suffix++;
|
||||||
|
|
||||||
|
return $"{preferred}{suffix}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
(generated by `dotnet ef migrations add Initial` against the
|
(generated by `dotnet ef migrations add Initial` against the
|
||||||
current entity model — captures all 67 `DbSet<>` tables).
|
current entity model — captures all 67 `DbSet<>` tables).
|
||||||
- **`LegacyUpgrades.cs`** holds idempotent data transforms that can't
|
- **`LegacyUpgrades.cs`** holds idempotent data transforms that can't
|
||||||
be derived from the entity model. Runs once per server boot, after
|
be derived from the entity model. Runs during host boot for the
|
||||||
EF migrations complete and before `RoomService.SeedAsync()`.
|
standalone server and for service-slice containers, after schema
|
||||||
|
bootstrap completes and before `RoomService.SeedAsync()`.
|
||||||
|
|
||||||
## What changed in the consolidation
|
## What changed in the consolidation
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ Both were collapsed in 2026-05-25:
|
|||||||
no per-feature patches needed.
|
no per-feature patches needed.
|
||||||
- The PhotonRegion coerce (the only data transform in the block)
|
- The PhotonRegion coerce (the only data transform in the block)
|
||||||
was moved into `LegacyUpgrades.CoercePhotonRegionAsync`.
|
was moved into `LegacyUpgrades.CoercePhotonRegionAsync`.
|
||||||
- The Storage admin page (`admin-ui/src/pages/Storage.tsx`) and
|
- The legacy storage backfill page (`admin-ui/src/pages/Storage.tsx`) and
|
||||||
`StorageBackfillController.cs` were deleted — they were the UI for
|
`StorageBackfillController.cs` were deleted — they were the UI for
|
||||||
draining the legacy `RoomDataBlobs.Bytes` column into S3, which is
|
draining the legacy `RoomDataBlobs.Bytes` column into S3, which is
|
||||||
no longer needed.
|
no longer needed.
|
||||||
@@ -37,8 +38,8 @@ Both were collapsed in 2026-05-25:
|
|||||||
## Production cutover (one-time SQL)
|
## Production cutover (one-time SQL)
|
||||||
|
|
||||||
Existing prod databases need a one-time fixup to tell EF "the new
|
Existing prod databases need a one-time fixup to tell EF "the new
|
||||||
Initial is already applied here" so the next boot doesn't try to
|
Initial is already applied here" so the next standalone or service-slice
|
||||||
`CREATE TABLE` over existing data.
|
boot doesn't try to `CREATE TABLE` over existing data.
|
||||||
|
|
||||||
### Postgres path
|
### Postgres path
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ table exists** because EF Core only creates it when `Migrate()` runs.
|
|||||||
Postgres continues to use `EnsureCreated()` post-consolidation, so
|
Postgres continues to use `EnsureCreated()` post-consolidation, so
|
||||||
there's *nothing to do* — `EnsureCreated()` reads the entity model,
|
there's *nothing to do* — `EnsureCreated()` reads the entity model,
|
||||||
sees the schema already matches, and no-ops. Just deploy the new
|
sees the schema already matches, and no-ops. Just deploy the new
|
||||||
build.
|
standalone image or microservices compose stack.
|
||||||
|
|
||||||
If you ever switch the Postgres path to `Migrate()`, run this once
|
If you ever switch the Postgres path to `Migrate()`, run this once
|
||||||
before the cutover deploy:
|
before the cutover deploy:
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
Generated: 2026-05-09. Source: client URLs from `Cpp2IL_ISIL` ISIL dump (curated list provided to this report); server routes harvested via grep of `[HttpGet]`/`[HttpPost]`/`[HttpPut]`/`[HttpDelete]`/`[Route]` attributes across `Controllers/**/*.cs` plus the namespace-wildcard fall-throughs in `ApiNamespaceStubsController` and the global `ApiCatchAllController` / `GlobalCatchAllController`.
|
Generated: 2026-05-09. Source: client URLs from `Cpp2IL_ISIL` ISIL dump (curated list provided to this report); server routes harvested via grep of `[HttpGet]`/`[HttpPost]`/`[HttpPut]`/`[HttpDelete]`/`[Route]` attributes across `Controllers/**/*.cs` plus the namespace-wildcard fall-throughs in `ApiNamespaceStubsController` and the global `ApiCatchAllController` / `GlobalCatchAllController`.
|
||||||
|
|
||||||
|
> Snapshot scope: this file is a historical controller-coverage matrix,
|
||||||
|
> not the live gateway route table. The dedicated microservice hosts
|
||||||
|
> still reuse the shared controller stack behind route guards, so the
|
||||||
|
> endpoint shape notes remain useful. For current service ownership, use
|
||||||
|
> `DorkNet.Contracts/DorkNetRouteOwnership.cs` or the gateway
|
||||||
|
> `/internal/routes` endpoint.
|
||||||
|
|
||||||
## How this matrix is built
|
## How this matrix is built
|
||||||
|
|
||||||
Each client URL is matched against the server's full route table. ASP.NET Core routes resolve "specific wins over wildcard", so a route literally registered as `api/foo/v1/bar` always wins over `api/foo/{*path}`. The `Status` column reflects the route that actually wins:
|
Each client URL is matched against the server's full route table. ASP.NET Core routes resolve "specific wins over wildcard", so a route literally registered as `api/foo/v1/bar` always wins over `api/foo/{*path}`. The `Status` column reflects the route that actually wins:
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
<PackageReference Include="Open.IdentityServer" Version="1.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.15" />
|
<PackageReference Include="MessagePack" Version="3.1.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||||
<!-- RenderedCompactJsonFormatter for stdout JSON logs in production
|
<!-- RenderedCompactJsonFormatter for stdout JSON logs in production
|
||||||
(Coolify-friendly). The host pipeline reads stdout line-by-line,
|
(Coolify-friendly). The host pipeline reads stdout line-by-line,
|
||||||
and structured JSON lets a future Loki/Datadog ingest parse fields
|
and structured JSON lets a future Loki/Datadog ingest parse fields
|
||||||
@@ -25,8 +27,8 @@
|
|||||||
scale: SignalR group fanout, OrphanAccountTracker (sticky-session
|
scale: SignalR group fanout, OrphanAccountTracker (sticky-session
|
||||||
account-creation flow), and PlayerPresenceService heartbeats all
|
account-creation flow), and PlayerPresenceService heartbeats all
|
||||||
need shared state across replicas. -->
|
need shared state across replicas. -->
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
|
<PackageReference Include="StackExchange.Redis" Version="2.13.17" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="10.0.9" />
|
||||||
<!-- S3-compatible object storage. Production target: Garage in
|
<!-- S3-compatible object storage. Production target: Garage in
|
||||||
Coolify (one-click). Dev fallback: local MinIO for parity
|
Coolify (one-click). Dev fallback: local MinIO for parity
|
||||||
testing. The SDK speaks plain S3, so swapping endpoints
|
testing. The SDK speaks plain S3, so swapping endpoints
|
||||||
@@ -38,7 +40,7 @@
|
|||||||
cost+salt in the output string so we only need one column. -->
|
cost+salt in the output string so we only need one column. -->
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||||
<!-- Server-side crop/resize for img.* CDN transforms (?cropSquare=1&width=N
|
<!-- Server-side crop/resize for img.* CDN transforms (?cropSquare=1&width=N
|
||||||
on profile thumbnails). The 2020 client treats this as part of the
|
on profile thumbnails). The 2020 client treats this as part of the
|
||||||
image CDN contract — it uploads a full-frame capture and expects the
|
image CDN contract — it uploads a full-frame capture and expects the
|
||||||
@@ -73,6 +75,16 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DorkNet.Models\DorkNet.Models.csproj" />
|
<ProjectReference Include="..\DorkNet.Models\DorkNet.Models.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Service-slice containers reference DorkNet.Server for shared controllers
|
||||||
|
and services, but each slice owns its own appsettings.json. Without this
|
||||||
|
guard, dotnet publish sees both files targeting appsettings.json in the
|
||||||
|
same publish root and fails NETSDK1152. -->
|
||||||
|
<ItemGroup Condition="'$(DorkNetServiceContainerPublish)' == 'true'">
|
||||||
|
<Content Update="appsettings*.json" CopyToPublishDirectory="Never" />
|
||||||
|
<None Update="appsettings*.json" CopyToPublishDirectory="Never" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Vite admin SPA build. Source at admin-ui/, output goes to
|
<!-- Vite admin SPA build. Source at admin-ui/, output goes to
|
||||||
|
|||||||
@@ -15,3 +15,5 @@ var app = builder.Build();
|
|||||||
await app.RunDatabaseBootstrapAsync();
|
await app.RunDatabaseBootstrapAsync();
|
||||||
app.UseDorkNetPipeline();
|
app.UseDorkNetPipeline();
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
public partial class Program;
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ using Microsoft.IdentityModel.Tokens;
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using DorkNet.Server.Auth;
|
||||||
using DorkNet.Server.Data;
|
using DorkNet.Server.Data;
|
||||||
|
|
||||||
namespace DorkNet.Server.Services;
|
namespace DorkNet.Server.Services;
|
||||||
|
|
||||||
public class AuthService(IConfiguration config, DorkNetDbContext db)
|
public class AuthService(IConfiguration config, DorkNetDbContext db, DomainConfig domain, IdentityServerSigningKeyProvider signingKeyProvider)
|
||||||
{
|
{
|
||||||
|
public const string AccessCookieName = "dorknet_access_token";
|
||||||
|
public const string LegacyIssuer = "https://api.rec.net/";
|
||||||
|
|
||||||
// Resolution order MUST match Program.cs's JwtBearer validator setup —
|
// Resolution order MUST match Program.cs's JwtBearer validator setup —
|
||||||
// env var first, config key second — otherwise tokens are signed with
|
// env var first, config key second — otherwise tokens are signed with
|
||||||
// one secret and validated against another and every authenticated
|
// one secret and validated against another and every authenticated
|
||||||
@@ -38,38 +42,17 @@ public class AuthService(IConfiguration config, DorkNetDbContext db)
|
|||||||
private string CreateJwt(long playerId, TimeSpan lifetime, string tokenType)
|
private string CreateJwt(long playerId, TimeSpan lifetime, string tokenType)
|
||||||
{
|
{
|
||||||
var handler = new JwtSecurityTokenHandler();
|
var handler = new JwtSecurityTokenHandler();
|
||||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));
|
var claims = RecRoomIdentityClaims.CreateAsync(db, playerId)
|
||||||
|
.GetAwaiter()
|
||||||
// Look up dev/admin flags so the JWT carries a "developer" role
|
.GetResult();
|
||||||
// claim for accounts that should see the in-game debug button.
|
claims.Add(new Claim("token_type", tokenType));
|
||||||
// The watch's Login.SetAccessToken parses the access_token's
|
|
||||||
// <c>role</c> claim — either a single string or a list — and
|
|
||||||
// calls <c>HasRole("developer", token)</c>; that result drives
|
|
||||||
// <c>Accounts.SetLocalAccountIsDev</c> and ultimately
|
|
||||||
// <c>SessionManager.IsDeveloper</c>, which the watch UI checks
|
|
||||||
// when deciding whether to show the dev console toggle.
|
|
||||||
var isDev = db.Players
|
|
||||||
.Where(p => p.Id == playerId)
|
|
||||||
.Select(p => (bool?)(p.IsDeveloper || p.IsAdmin))
|
|
||||||
.FirstOrDefault() ?? false;
|
|
||||||
|
|
||||||
var claims = new List<Claim>
|
|
||||||
{
|
|
||||||
new(ClaimTypes.NameIdentifier, playerId.ToString()),
|
|
||||||
new(ClaimTypes.Role, "gameClient"),
|
|
||||||
new("token_type", tokenType),
|
|
||||||
};
|
|
||||||
// Multiple Role claims become a JSON array on the wire — the
|
|
||||||
// watch's HasRole helper handles both string and List<string>
|
|
||||||
// forms.
|
|
||||||
if (isDev) claims.Add(new Claim(ClaimTypes.Role, "developer"));
|
|
||||||
|
|
||||||
var descriptor = new SecurityTokenDescriptor
|
var descriptor = new SecurityTokenDescriptor
|
||||||
{
|
{
|
||||||
Subject = new ClaimsIdentity(claims),
|
Subject = new ClaimsIdentity(claims),
|
||||||
Expires = DateTime.UtcNow.Add(lifetime),
|
Expires = DateTime.UtcNow.Add(lifetime),
|
||||||
Issuer = "https://api.rec.net/",
|
Issuer = domain.AuthIssuer,
|
||||||
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256),
|
SigningCredentials = new SigningCredentials(signingKeyProvider.SigningKey, SecurityAlgorithms.RsaSha256),
|
||||||
};
|
};
|
||||||
|
|
||||||
return handler.WriteToken(handler.CreateJwtSecurityToken(descriptor));
|
return handler.WriteToken(handler.CreateJwtSecurityToken(descriptor));
|
||||||
@@ -80,19 +63,22 @@ public class AuthService(IConfiguration config, DorkNetDbContext db)
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var handler = new JwtSecurityTokenHandler();
|
var handler = new JwtSecurityTokenHandler();
|
||||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));
|
|
||||||
|
|
||||||
var principal = handler.ValidateToken(token, new TokenValidationParameters
|
var principal = handler.ValidateToken(token, new TokenValidationParameters
|
||||||
{
|
{
|
||||||
ValidateIssuerSigningKey = true,
|
ValidateIssuerSigningKey = true,
|
||||||
IssuerSigningKey = key,
|
IssuerSigningKeys = signingKeyProvider.ValidationKeys,
|
||||||
ValidateIssuer = true,
|
ValidateIssuer = true,
|
||||||
ValidIssuer = "https://api.rec.net/",
|
ValidIssuers = new[] { domain.AuthIssuer, LegacyIssuer },
|
||||||
ValidateAudience = false,
|
ValidateAudience = false,
|
||||||
ClockSkew = TimeSpan.Zero,
|
ClockSkew = TimeSpan.Zero,
|
||||||
}, out _);
|
}, out _);
|
||||||
|
|
||||||
var idClaim = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
var idClaim = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
||||||
|
?? principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value
|
||||||
|
?? principal.FindFirst("sub")?.Value
|
||||||
|
?? principal.FindFirst("accountId")?.Value
|
||||||
|
?? principal.FindFirst("account_id")?.Value
|
||||||
|
?? principal.FindFirst("accountid")?.Value;
|
||||||
return idClaim is not null ? long.Parse(idClaim) : null;
|
return idClaim is not null ? long.Parse(idClaim) : null;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ public class ConfigService(IConfiguration config, DomainConfig domain)
|
|||||||
return new RecRoomConfig
|
return new RecRoomConfig
|
||||||
{
|
{
|
||||||
MessageOfTheDay = config["Server:MOTD"] ?? "Welcome to the private server!",
|
MessageOfTheDay = config["Server:MOTD"] ?? "Welcome to the private server!",
|
||||||
CdnBaseUri = $"https://{domain.Sub("cdn")}",
|
CdnBaseUri = domain.Url("cdn"),
|
||||||
PhotonConfig = photon,
|
PhotonConfig = photon,
|
||||||
ServiceUrls = BuildServiceUrlMap(apex),
|
ServiceUrls = BuildServiceUrlMap(domain),
|
||||||
ConfigTable = new Dictionary<string, string>
|
ConfigTable = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
// Season keys the 2019/2020 client checks at startup
|
// Season keys the 2019/2020 client checks at startup
|
||||||
@@ -110,12 +110,14 @@ public class ConfigService(IConfiguration config, DomainConfig domain)
|
|||||||
/// <see cref="DomainConfig.Apex"/>) so the map stays a single
|
/// <see cref="DomainConfig.Apex"/>) so the map stays a single
|
||||||
/// source of truth.</summary>
|
/// source of truth.</summary>
|
||||||
public static Dictionary<string, string> BuildServiceUrlMap(string apex)
|
public static Dictionary<string, string> BuildServiceUrlMap(string apex)
|
||||||
|
=> BuildServiceUrlMap(new DomainConfig(apex));
|
||||||
|
|
||||||
|
public static Dictionary<string, string> BuildServiceUrlMap(DomainConfig domain)
|
||||||
{
|
{
|
||||||
var map = new Dictionary<string, string>(ServiceSubdomains.Length);
|
var map = new Dictionary<string, string>(ServiceSubdomains.Length);
|
||||||
foreach (var (service, sub) in ServiceSubdomains)
|
foreach (var (service, sub) in ServiceSubdomains)
|
||||||
{
|
{
|
||||||
var host = string.IsNullOrEmpty(sub) ? apex : $"{sub}.{apex}";
|
map[service] = domain.Url(sub);
|
||||||
map[service] = $"https://{host}";
|
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,31 @@ namespace DorkNet.Server.Services;
|
|||||||
public sealed class DomainConfig
|
public sealed class DomainConfig
|
||||||
{
|
{
|
||||||
public string Apex { get; }
|
public string Apex { get; }
|
||||||
public DomainConfig(string apex) { Apex = apex; }
|
public string Scheme { get; }
|
||||||
|
|
||||||
|
public DomainConfig(string apex, string scheme = "https")
|
||||||
|
{
|
||||||
|
Apex = apex;
|
||||||
|
Scheme = NormalizeScheme(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
public string Sub(string prefix) => $"{prefix}.{Apex}";
|
public string Sub(string prefix) => $"{prefix}.{Apex}";
|
||||||
|
|
||||||
|
public string AuthIssuer => Url("auth");
|
||||||
|
|
||||||
|
public string Url(string prefix, string path = "")
|
||||||
|
{
|
||||||
|
var host = string.IsNullOrEmpty(prefix) ? Apex : Sub(prefix);
|
||||||
|
if (string.IsNullOrEmpty(path)) return $"{Scheme}://{host}";
|
||||||
|
return $"{Scheme}://{host}{(path.StartsWith('/') ? path : "/" + path)}";
|
||||||
|
}
|
||||||
|
|
||||||
public static bool MatchesSubdomain(string host, string prefix)
|
public static bool MatchesSubdomain(string host, string prefix)
|
||||||
=> host.StartsWith(prefix + ".", StringComparison.OrdinalIgnoreCase);
|
=> host.StartsWith(prefix + ".", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static string NormalizeScheme(string? value)
|
||||||
|
{
|
||||||
|
var scheme = string.IsNullOrWhiteSpace(value) ? "https" : value.Trim().TrimEnd(':', '/', '\\').ToLowerInvariant();
|
||||||
|
return scheme is "http" or "https" ? scheme : "https";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,12 @@ public class GameSessionService(DorkNetDbContext db)
|
|||||||
// in, so we save once with a placeholder, then patch and save
|
// in, so we save once with a placeholder, then patch and save
|
||||||
// again. Two SaveChangesAsync calls is cheap at our QPS.
|
// again. Two SaveChangesAsync calls is cheap at our QPS.
|
||||||
PhotonRoomName = "pending",
|
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,
|
PlayerCount = 1,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
};
|
};
|
||||||
@@ -75,6 +80,28 @@ public class GameSessionService(DorkNetDbContext db)
|
|||||||
return ToDto(session);
|
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)
|
public async Task<GameSession?> GetByIdAsync(long id)
|
||||||
{
|
{
|
||||||
var s = await db.GameSessions.AsNoTracking()
|
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) &&
|
if (ctx.Request.Host.Host.Equals(domainCfg.Sub("api"), StringComparison.OrdinalIgnoreCase) &&
|
||||||
(ctx.Request.Path == "/admin" || ctx.Request.Path.StartsWithSegments("/admin")))
|
(ctx.Request.Path == "/admin" || ctx.Request.Path.StartsWithSegments("/admin")))
|
||||||
{
|
{
|
||||||
ctx.Response.Redirect($"https://{domainCfg.Sub("admin")}/", permanent: false);
|
ctx.Response.Redirect(domainCfg.Url("admin", "/"), permanent: false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
@@ -87,6 +87,9 @@ public static class MiddlewarePipelineExtensions
|
|||||||
// calls (login, photon custom auth) are gated too.
|
// calls (login, photon custom auth) are gated too.
|
||||||
app.UseMiddleware<VersionDetectionMiddleware>();
|
app.UseMiddleware<VersionDetectionMiddleware>();
|
||||||
|
|
||||||
|
app.UseMiddleware<IdentityServerGameTokenRequestMiddleware>();
|
||||||
|
app.UseIdentityServer();
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
// Sits between auth and the controllers — by this point ctx.User has
|
// Sits between auth and the controllers — by this point ctx.User has
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.Server.Auth;
|
||||||
using DorkNet.Server.Compat;
|
using DorkNet.Server.Compat;
|
||||||
using DorkNet.Server.Data;
|
using DorkNet.Server.Data;
|
||||||
using DorkNet.Server.Services;
|
using DorkNet.Server.Services;
|
||||||
@@ -23,11 +25,11 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
ConfigureLogging(builder);
|
ConfigureLogging(builder);
|
||||||
AddDatabase(builder);
|
AddDatabase(builder);
|
||||||
AddDomain(builder);
|
var domain = AddDomain(builder);
|
||||||
AddCoreServices(builder.Services);
|
AddCoreServices(builder.Services);
|
||||||
AddVersionCompatibility(builder);
|
AddVersionCompatibility(builder);
|
||||||
var redisConn = AddRedis(builder);
|
var redisConn = AddRedis(builder);
|
||||||
AddAuthentication(builder);
|
AddAuthentication(builder, domain);
|
||||||
AddKestrelAndSignalR(builder, redisConn);
|
AddKestrelAndSignalR(builder, redisConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,20 +102,27 @@ public static class ServiceCollectionExtensions
|
|||||||
// Replaces per-controller [Host(...)] filters that hard-coded
|
// Replaces per-controller [Host(...)] filters that hard-coded
|
||||||
// rec.net/localhost pairs — the singleton DomainConfig is injected
|
// rec.net/localhost pairs — the singleton DomainConfig is injected
|
||||||
// anywhere code needs to build outbound URLs.
|
// anywhere code needs to build outbound URLs.
|
||||||
private static void AddDomain(WebApplicationBuilder builder)
|
private static DomainConfig AddDomain(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
var apex =
|
var apex =
|
||||||
builder.Configuration["Domain:Apex"]
|
builder.Configuration["Domain:Apex"]
|
||||||
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
|
?? Environment.GetEnvironmentVariable("DORKNET_DOMAIN")
|
||||||
?? "localhost";
|
?? "localhost";
|
||||||
builder.Services.AddSingleton(new DomainConfig(apex));
|
var scheme = builder.Configuration["Domain:Scheme"] ?? "https";
|
||||||
|
var domain = new DomainConfig(apex, scheme);
|
||||||
|
builder.Services.AddSingleton(domain);
|
||||||
|
var allowedHosts = new[] { apex, "localhost", "127.0.0.1" }
|
||||||
|
.Concat(DorkNetRouteOwnership.PublicSubdomains.Select(subdomain => $"{subdomain}.{apex}"))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
builder.Services.Configure<Microsoft.AspNetCore.HostFiltering.HostFilteringOptions>(opt =>
|
builder.Services.Configure<Microsoft.AspNetCore.HostFiltering.HostFilteringOptions>(opt =>
|
||||||
{
|
{
|
||||||
opt.AllowedHosts = new[] { apex, $"*.{apex}", "localhost", "127.0.0.1" };
|
opt.AllowedHosts = allowedHosts;
|
||||||
opt.AllowEmptyHosts = true;
|
opt.AllowEmptyHosts = true;
|
||||||
opt.IncludeFailureMessage = true;
|
opt.IncludeFailureMessage = true;
|
||||||
});
|
});
|
||||||
Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{apex}, *.{apex}, localhost]");
|
Console.WriteLine($"[domain] apex={apex}, allowedHosts=[{string.Join(", ", allowedHosts)}]");
|
||||||
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Services ──────────────────────────────────────────────────────────────
|
// ── Services ──────────────────────────────────────────────────────────────
|
||||||
@@ -246,15 +255,11 @@ public static class ServiceCollectionExtensions
|
|||||||
// Never commit a real secret to appsettings.json. DORKNET_JWT_SECRET is the
|
// Never commit a real secret to appsettings.json. DORKNET_JWT_SECRET is the
|
||||||
// canonical env var name; RECNET_JWT_SECRET kept for backward compat with
|
// canonical env var name; RECNET_JWT_SECRET kept for backward compat with
|
||||||
// older Coolify configs.
|
// older Coolify configs.
|
||||||
private static void AddAuthentication(WebApplicationBuilder builder)
|
private static void AddAuthentication(WebApplicationBuilder builder, DomainConfig domain)
|
||||||
{
|
{
|
||||||
var jwtSecret = Environment.GetEnvironmentVariable("DORKNET_JWT_SECRET")
|
var signingKeyProvider = new IdentityServerSigningKeyProvider(builder.Configuration);
|
||||||
?? Environment.GetEnvironmentVariable("RECNET_JWT_SECRET")
|
builder.Services.AddSingleton(signingKeyProvider);
|
||||||
?? builder.Configuration["Jwt:Secret"]
|
builder.Services.AddRecRoomIdentityServer(builder.Configuration, domain, signingKeyProvider);
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
"JWT secret not configured. Set the DORKNET_JWT_SECRET env var or " +
|
|
||||||
"add `Jwt:Secret` to appsettings.Local.json (the install-plugin.ps1 " +
|
|
||||||
"script generates one automatically on same-machine setup).");
|
|
||||||
|
|
||||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
.AddJwtBearer(opt =>
|
.AddJwtBearer(opt =>
|
||||||
@@ -262,9 +267,9 @@ public static class ServiceCollectionExtensions
|
|||||||
opt.TokenValidationParameters = new TokenValidationParameters
|
opt.TokenValidationParameters = new TokenValidationParameters
|
||||||
{
|
{
|
||||||
ValidateIssuerSigningKey = true,
|
ValidateIssuerSigningKey = true,
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
|
IssuerSigningKeys = signingKeyProvider.ValidationKeys,
|
||||||
ValidateIssuer = true,
|
ValidateIssuer = true,
|
||||||
ValidIssuer = "https://api.rec.net/",
|
ValidIssuers = new[] { domain.AuthIssuer, AuthService.LegacyIssuer },
|
||||||
ValidateAudience = false,
|
ValidateAudience = false,
|
||||||
ClockSkew = TimeSpan.Zero,
|
ClockSkew = TimeSpan.Zero,
|
||||||
};
|
};
|
||||||
@@ -284,6 +289,12 @@ public static class ServiceCollectionExtensions
|
|||||||
var path = ctx.HttpContext.Request.Path;
|
var path = ctx.HttpContext.Request.Path;
|
||||||
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hub"))
|
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hub"))
|
||||||
ctx.Token = accessToken;
|
ctx.Token = accessToken;
|
||||||
|
if (string.IsNullOrEmpty(ctx.Token) &&
|
||||||
|
ctx.Request.Cookies.TryGetValue(AuthService.AccessCookieName, out var cookieToken) &&
|
||||||
|
!string.IsNullOrWhiteSpace(cookieToken))
|
||||||
|
{
|
||||||
|
ctx.Token = cookieToken;
|
||||||
|
}
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
|
<meta name="theme-color" content="#0b95c7" />
|
||||||
<title>DorkNet Admin</title>
|
<title>DorkNet Admin</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="%BASE_URL%brand/dorknet.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
import logoUrl from '../assets/dorknet.svg';
|
||||||
|
|
||||||
|
export function BrandMark({ className = 'size-8' }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex shrink-0 items-center justify-center rounded-md bg-ink-950 ring-1 ring-brand-300/30 shadow-lg shadow-brand-900/30 ${className}`}>
|
||||||
|
<img src={logoUrl} alt="" className="h-full w-full object-contain p-0.5" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
LayoutDashboard, Users, Building2, Activity,
|
LayoutDashboard, Users, Building2, Activity,
|
||||||
Megaphone, MessageSquare, Settings, X,
|
Megaphone, MessageSquare, Settings, X,
|
||||||
} from './Icons';
|
} from './Icons';
|
||||||
|
import { BrandMark } from './BrandMark';
|
||||||
|
|
||||||
// Consolidated navigation. Per-player moderation (bans, grants, gifts,
|
// Consolidated navigation. Per-player moderation (bans, grants, gifts,
|
||||||
// password resets) lives in the player detail modal; the old Bans /
|
// password resets) lives in the player detail modal; the old Bans /
|
||||||
@@ -67,9 +68,9 @@ export function Sidebar({ open, onClose, onNavigate }: { open: boolean; onClose:
|
|||||||
<>
|
<>
|
||||||
<div className="px-2 pb-4 flex items-center justify-between">
|
<div className="px-2 pb-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="size-8 rounded-md bg-gradient-to-br from-brand-400 to-brand-600 flex items-center justify-center font-bold text-white">D</div>
|
<BrandMark className="size-9" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-semibold tracking-tight text-ink-50">DorkNet</div>
|
<div className="text-sm font-semibold text-ink-50">DorkNet</div>
|
||||||
<div className="text-[10px] uppercase tracking-widest text-ink-400">admin</div>
|
<div className="text-[10px] uppercase tracking-widest text-ink-400">admin</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { get } from '../lib/api';
|
||||||
|
import { RefreshCw } from '../components/Icons';
|
||||||
|
|
||||||
|
interface IdentityServerSettings {
|
||||||
|
issuer: string;
|
||||||
|
discoveryUrl: string;
|
||||||
|
tokenEndpoint: string;
|
||||||
|
userInfoEndpoint: string;
|
||||||
|
client: {
|
||||||
|
clientId: string;
|
||||||
|
clientName: string;
|
||||||
|
allowedGrantTypes: string[];
|
||||||
|
allowedScopes: string[];
|
||||||
|
allowOfflineAccess: boolean;
|
||||||
|
accessTokenLifetime: number;
|
||||||
|
absoluteRefreshTokenLifetime: number;
|
||||||
|
slidingRefreshTokenLifetime: number;
|
||||||
|
refreshTokenUsage: string;
|
||||||
|
refreshTokenExpiration: string;
|
||||||
|
};
|
||||||
|
signing: {
|
||||||
|
certificateSource: string;
|
||||||
|
loadedFromBase64: boolean;
|
||||||
|
certificatePath: string;
|
||||||
|
certificateExists: boolean;
|
||||||
|
thumbprint: string;
|
||||||
|
notBeforeUtc: string;
|
||||||
|
notAfterUtc: string;
|
||||||
|
algorithm: string;
|
||||||
|
};
|
||||||
|
persistedGrants: {
|
||||||
|
store: string;
|
||||||
|
warning: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IdentityServer() {
|
||||||
|
const [settings, setSettings] = useState<IdentityServerSettings | null>(null);
|
||||||
|
const [err, setErr] = useState<string | null>(null);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const data = await get<IdentityServerSettings>('/identityserver');
|
||||||
|
setSettings(data);
|
||||||
|
setErr(null);
|
||||||
|
} catch (e) {
|
||||||
|
setErr((e as Error).message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => { void load(); }, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-3 flex justify-end">
|
||||||
|
<button onClick={load} className="btn-secondary text-xs" disabled={busy}>
|
||||||
|
<RefreshCw className={busy ? 'animate-spin' : ''} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{err && (
|
||||||
|
<div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger mb-4">{err}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!settings && !err && (
|
||||||
|
<div className="card !p-6 text-center text-xs text-ink-400">Loading...</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{settings && (
|
||||||
|
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="card !p-5">
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold text-ink-50">IdentityServer</h2>
|
||||||
|
<p className="mt-1 break-all font-mono text-xs text-ink-300">{settings.issuer}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 text-xs">
|
||||||
|
<StatusBadge ok={settings.signing.certificateExists} on="Signing key" off="Missing key" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 grid gap-3 md:grid-cols-3">
|
||||||
|
<Endpoint label="Discovery" value={settings.discoveryUrl} />
|
||||||
|
<Endpoint label="Token" value={settings.tokenEndpoint} />
|
||||||
|
<Endpoint label="UserInfo" value={settings.userInfoEndpoint} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card !p-5">
|
||||||
|
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold text-ink-50">Client</h2>
|
||||||
|
<p className="mt-1 font-mono text-xs text-ink-400">{settings.client.clientId}</p>
|
||||||
|
</div>
|
||||||
|
<span className={settings.client.allowOfflineAccess ? 'badge-online' : 'badge-neutral'}>
|
||||||
|
Offline access {settings.client.allowOfflineAccess ? 'on' : 'off'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
|
<ChipList title="Grant types" values={settings.client.allowedGrantTypes} />
|
||||||
|
<ChipList title="Scopes" values={settings.client.allowedScopes} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 grid gap-3 sm:grid-cols-3">
|
||||||
|
<Metric label="Access token" value={duration(settings.client.accessTokenLifetime)} />
|
||||||
|
<Metric label="Refresh token" value={duration(settings.client.absoluteRefreshTokenLifetime)} />
|
||||||
|
<Metric label="Refresh mode" value={`${settings.client.refreshTokenUsage} / ${settings.client.refreshTokenExpiration}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="card !p-5">
|
||||||
|
<h2 className="text-sm font-semibold text-ink-50">Signing certificate</h2>
|
||||||
|
<div className="mt-4 space-y-3">
|
||||||
|
<Detail label="Source" value={settings.signing.certificateSource} mono />
|
||||||
|
{!settings.signing.loadedFromBase64 && (
|
||||||
|
<Detail label="Path" value={settings.signing.certificatePath} mono />
|
||||||
|
)}
|
||||||
|
<Detail label="Thumbprint" value={settings.signing.thumbprint} mono />
|
||||||
|
<Detail label="Algorithm" value={settings.signing.algorithm} mono />
|
||||||
|
<Detail label="Valid from" value={formatDate(settings.signing.notBeforeUtc)} />
|
||||||
|
<Detail label="Valid until" value={formatDate(settings.signing.notAfterUtc)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card !p-5">
|
||||||
|
<h2 className="text-sm font-semibold text-ink-50">Operational store</h2>
|
||||||
|
<div className="mt-3 flex items-center justify-between gap-3">
|
||||||
|
<span className="text-xs text-ink-400">Persisted grants</span>
|
||||||
|
<span className="badge-junior">{settings.persistedGrants.store}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-xs leading-5 text-ink-400">{settings.persistedGrants.warning}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Endpoint({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-ink-800 bg-ink-950/40 p-3">
|
||||||
|
<div className="label">{label}</div>
|
||||||
|
<div className="mt-2 break-all font-mono text-xs text-ink-100">{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChipList({ title, values }: { title: string; values: string[] }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-ink-800 bg-ink-950/30 p-4">
|
||||||
|
<h3 className="text-xs font-semibold uppercase text-ink-400">{title}</h3>
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{values.map((value) => (
|
||||||
|
<span key={value} className="badge-neutral font-mono normal-case tracking-normal">{value}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Metric({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-ink-800 bg-ink-950/30 p-3">
|
||||||
|
<div className="text-[11px] uppercase text-ink-500">{label}</div>
|
||||||
|
<div className="mt-1 text-sm font-semibold text-ink-50">{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="label">{label}</div>
|
||||||
|
<div className={(mono ? 'font-mono ' : '') + 'mt-1 break-all text-xs text-ink-100'}>{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge({ ok, on, off }: { ok: boolean; on: string; off: string }) {
|
||||||
|
return <span className={ok ? 'badge-online' : 'badge-junior'}>{ok ? on : off}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value: string): string {
|
||||||
|
const date = new Date(value);
|
||||||
|
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function duration(seconds: number): string {
|
||||||
|
if (!Number.isFinite(seconds) || seconds <= 0) return '0s';
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
if (days > 0) return `${days}d ${hours}h`;
|
||||||
|
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||||
|
if (minutes > 0) return `${minutes}m`;
|
||||||
|
return `${seconds}s`;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { api, ApiError } from '../lib/api';
|
import { api, ApiError } from '../lib/api';
|
||||||
import { notifyAuthChange, setSession } from '../lib/auth';
|
import { notifyAuthChange, setSession } from '../lib/auth';
|
||||||
|
import { BrandMark } from '../components/BrandMark';
|
||||||
|
|
||||||
interface LoginResponse {
|
interface LoginResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
@@ -51,12 +52,12 @@ export function Login() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-full flex items-center justify-center px-4 py-12 bg-gradient-to-br from-ink-950 via-ink-950 to-ink-900">
|
<div className="min-h-full flex items-center justify-center px-4 py-12 bg-[linear-gradient(135deg,#0a0b0e_0%,#0d1d25_48%,#131418_100%)]">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm">
|
||||||
<div className="mb-8 flex flex-col items-center gap-2">
|
<div className="mb-8 flex flex-col items-center gap-2">
|
||||||
<div className="size-12 rounded-xl bg-gradient-to-br from-brand-400 to-brand-600 flex items-center justify-center text-xl font-bold text-white shadow-lg shadow-brand-900/40">D</div>
|
<BrandMark className="size-16 rounded-xl" />
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-xl font-semibold tracking-tight text-ink-50">DorkNet Admin</h1>
|
<h1 className="text-xl font-semibold text-ink-50">DorkNet Admin</h1>
|
||||||
<p className="text-xs text-ink-400">Sign in with an admin account.</p>
|
<p className="text-xs text-ink-400">Sign in with an admin account.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Settings } from './Settings';
|
import { Settings } from './Settings';
|
||||||
|
import { IdentityServer } from './IdentityServer';
|
||||||
import { SignupCodes } from './SignupCodes';
|
import { SignupCodes } from './SignupCodes';
|
||||||
import { PageHeader } from '../components/PageHeader';
|
import { PageHeader } from '../components/PageHeader';
|
||||||
import { Tabs, useTabParam } from '../components/Tabs';
|
import { Tabs, useTabParam } from '../components/Tabs';
|
||||||
@@ -7,19 +8,21 @@ import { Tabs, useTabParam } from '../components/Tabs';
|
|||||||
// invite codes (an access-control setting in its own right). Old
|
// invite codes (an access-control setting in its own right). Old
|
||||||
// /signup-codes route redirects here with ?tab=signup.
|
// /signup-codes route redirects here with ?tab=signup.
|
||||||
export function SettingsHome() {
|
export function SettingsHome() {
|
||||||
const [tab, setTab] = useTabParam<'server' | 'signup'>('server', ['server', 'signup']);
|
const [tab, setTab] = useTabParam<'server' | 'identity' | 'signup'>('server', ['server', 'identity', 'signup']);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader title="Settings" blurb="Runtime server toggles and single-use signup invite codes." />
|
<PageHeader title="Settings" blurb="Runtime server toggles, authentication, and single-use signup invite codes." />
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={[
|
tabs={[
|
||||||
{ key: 'server', label: 'Server' },
|
{ key: 'server', label: 'Server' },
|
||||||
|
{ key: 'identity', label: 'IdentityServer' },
|
||||||
{ key: 'signup', label: 'Signup codes' },
|
{ key: 'signup', label: 'Signup codes' },
|
||||||
]}
|
]}
|
||||||
active={tab}
|
active={tab}
|
||||||
onChange={setTab}
|
onChange={setTab}
|
||||||
/>
|
/>
|
||||||
{tab === 'server' && <Settings embedded />}
|
{tab === 'server' && <Settings embedded />}
|
||||||
|
{tab === 'identity' && <IdentityServer />}
|
||||||
{tab === 'signup' && <SignupCodes embedded />}
|
{tab === 'signup' && <SignupCodes embedded />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -20,17 +20,17 @@ export default {
|
|||||||
950: '#0a0b0e',
|
950: '#0a0b0e',
|
||||||
},
|
},
|
||||||
brand: {
|
brand: {
|
||||||
DEFAULT: '#7c5cff',
|
DEFAULT: '#0b95c7',
|
||||||
50: '#f1edff',
|
50: '#ecfbff',
|
||||||
100: '#e1d8ff',
|
100: '#c8f3ff',
|
||||||
200: '#c5b6ff',
|
200: '#8bedff',
|
||||||
300: '#a48dff',
|
300: '#51e5ff',
|
||||||
400: '#8d72ff',
|
400: '#22cfee',
|
||||||
500: '#7c5cff',
|
500: '#0b95c7',
|
||||||
600: '#6948e3',
|
600: '#087aa8',
|
||||||
700: '#5236b8',
|
700: '#096083',
|
||||||
800: '#3e2989',
|
800: '#0a465f',
|
||||||
900: '#2a1c5f',
|
900: '#082f40',
|
||||||
},
|
},
|
||||||
success: '#3ecf8e',
|
success: '#3ecf8e',
|
||||||
warn: '#f4b740',
|
warn: '#f4b740',
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
|
<meta name="theme-color" content="#0b95c7" />
|
||||||
<title>DorkNet</title>
|
<title>DorkNet</title>
|
||||||
<meta name="description" content="A private Rec Room server. Browse the player photo feed, find friends, and explore community-made rooms." />
|
<meta name="description" content="A private Rec Room server. Browse the player photo feed, find friends, and explore community-made rooms." />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="%BASE_URL%brand/dorknet.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
import logoUrl from '../assets/dorknet.svg';
|
||||||
|
|
||||||
|
export function BrandMark({ className = 'size-8' }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex shrink-0 items-center justify-center rounded-md bg-ink-950 ring-1 ring-brand-300/30 shadow-lg shadow-brand-900/30 ${className}`}>
|
||||||
|
<img src={logoUrl} alt="" className="h-full w-full object-contain p-0.5" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NavLink, Link, useLocation, useNavigate } from 'react-router-dom';
|
import { NavLink, Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { BrandMark } from './BrandMark';
|
||||||
|
|
||||||
const NAV = [
|
const NAV = [
|
||||||
{ to: '/', label: 'Home', exact: true },
|
{ to: '/', label: 'Home', exact: true },
|
||||||
@@ -30,8 +31,8 @@ export function Header() {
|
|||||||
<header className="sticky top-0 z-40 border-b border-ink-800 bg-ink-950/85 backdrop-blur">
|
<header className="sticky top-0 z-40 border-b border-ink-800 bg-ink-950/85 backdrop-blur">
|
||||||
<div className="mx-auto max-w-6xl flex items-center gap-4 px-4 sm:px-6 py-3">
|
<div className="mx-auto max-w-6xl flex items-center gap-4 px-4 sm:px-6 py-3">
|
||||||
<Link to="/" className="flex items-center gap-2 shrink-0">
|
<Link to="/" className="flex items-center gap-2 shrink-0">
|
||||||
<span className="inline-block size-6 rounded-md bg-gradient-to-br from-brand-400 to-brand-700 shadow-inner" />
|
<BrandMark className="size-7" />
|
||||||
<span className="font-semibold text-ink-50 tracking-tight">DorkNet</span>
|
<span className="font-semibold text-ink-50">DorkNet</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="hidden md:flex items-center gap-1 ml-2">
|
<nav className="hidden md:flex items-center gap-1 ml-2">
|
||||||
|
|||||||
@@ -16,11 +16,9 @@ export function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
<section className="card !p-6 sm:!p-10 relative overflow-hidden">
|
<section className="card !p-6 sm:!p-10 relative overflow-hidden border-brand-500/20 bg-[linear-gradient(135deg,rgba(11,149,199,0.16),rgba(19,20,24,0.72)_42%,rgba(19,20,24,0.92))]">
|
||||||
<div className="absolute -top-20 -right-24 size-72 rounded-full bg-brand-500/20 blur-3xl pointer-events-none" />
|
|
||||||
<div className="absolute -bottom-20 -left-24 size-72 rounded-full bg-brand-700/20 blur-3xl pointer-events-none" />
|
|
||||||
<div className="relative max-w-2xl">
|
<div className="relative max-w-2xl">
|
||||||
<h1 className="text-3xl sm:text-5xl font-bold text-ink-50 tracking-tight">
|
<h1 className="text-3xl sm:text-5xl font-bold text-ink-50">
|
||||||
Rec Room, kept alive.
|
Rec Room, kept alive.
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-3 text-ink-300 text-base sm:text-lg">
|
<p className="mt-3 text-ink-300 text-base sm:text-lg">
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -20,17 +20,17 @@ export default {
|
|||||||
950: '#0a0b0e',
|
950: '#0a0b0e',
|
||||||
},
|
},
|
||||||
brand: {
|
brand: {
|
||||||
DEFAULT: '#7c5cff',
|
DEFAULT: '#0b95c7',
|
||||||
50: '#f1edff',
|
50: '#ecfbff',
|
||||||
100: '#e1d8ff',
|
100: '#c8f3ff',
|
||||||
200: '#c5b6ff',
|
200: '#8bedff',
|
||||||
300: '#a48dff',
|
300: '#51e5ff',
|
||||||
400: '#8d72ff',
|
400: '#22cfee',
|
||||||
500: '#7c5cff',
|
500: '#0b95c7',
|
||||||
600: '#6948e3',
|
600: '#087aa8',
|
||||||
700: '#5236b8',
|
700: '#096083',
|
||||||
800: '#3e2989',
|
800: '#0a465f',
|
||||||
900: '#2a1c5f',
|
900: '#082f40',
|
||||||
},
|
},
|
||||||
success: '#3ecf8e',
|
success: '#3ecf8e',
|
||||||
warn: '#f4b740',
|
warn: '#f4b740',
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -3,12 +3,17 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#0b95c7">
|
||||||
<title>DorkNet Admin</title>
|
<title>DorkNet Admin</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="brand/dorknet.svg">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="login" class="card">
|
<div id="login" class="card">
|
||||||
<h1>DorkNet Admin</h1>
|
<div class="brand-lockup">
|
||||||
|
<span class="logo-mark"><img src="brand/dorknet.svg" alt=""></span>
|
||||||
|
<h1>DorkNet Admin</h1>
|
||||||
|
</div>
|
||||||
<p class="muted">Sign in with an admin account.</p>
|
<p class="muted">Sign in with an admin account.</p>
|
||||||
<form id="loginForm">
|
<form id="loginForm">
|
||||||
<label>Username<input id="loginUsername" autocomplete="username" required></label>
|
<label>Username<input id="loginUsername" autocomplete="username" required></label>
|
||||||
@@ -20,7 +25,10 @@
|
|||||||
|
|
||||||
<div id="app" hidden>
|
<div id="app" hidden>
|
||||||
<header>
|
<header>
|
||||||
<h1>DorkNet Admin</h1>
|
<div class="brand-lockup">
|
||||||
|
<span class="logo-mark"><img src="brand/dorknet.svg" alt=""></span>
|
||||||
|
<h1>DorkNet Admin</h1>
|
||||||
|
</div>
|
||||||
<div class="who">
|
<div class="who">
|
||||||
Signed in as <span id="meName"></span>
|
Signed in as <span id="meName"></span>
|
||||||
<button id="logoutBtn" class="link">log out</button>
|
<button id="logoutBtn" class="link">log out</button>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
|
||||||
--bg: #0a0c10;
|
--bg: #0a0c10;
|
||||||
--bg-grad: radial-gradient(circle at 20% 0%, #16204a 0%, #0a0c10 60%);
|
--bg-grad: radial-gradient(circle at 18% -8%, rgba(11, 149, 199, 0.22) 0%, rgba(10, 12, 16, 0) 42%), linear-gradient(135deg, #0a0c10 0%, #0d1d25 48%, #0a0c10 100%);
|
||||||
--panel: rgba(22, 26, 35, 0.85);
|
--panel: rgba(22, 26, 35, 0.85);
|
||||||
--panel-solid: #161a23;
|
--panel-solid: #161a23;
|
||||||
--panel-2: #1d2230;
|
--panel-2: #1d2230;
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
--text-dim: #b3b9c9;
|
--text-dim: #b3b9c9;
|
||||||
--muted: #7a8195;
|
--muted: #7a8195;
|
||||||
|
|
||||||
--accent: #7aa9ff;
|
--accent: #33ebff;
|
||||||
--accent-2: #b48cff;
|
--accent-2: #0b95c7;
|
||||||
--accent-grad: linear-gradient(135deg, #7aa9ff 0%, #b48cff 100%);
|
--accent-grad: linear-gradient(135deg, #33ebff 0%, #0b95c7 100%);
|
||||||
--accent-hover: #93b9ff;
|
--accent-hover: #c8f3ff;
|
||||||
|
|
||||||
--good: #4ad28b;
|
--good: #4ad28b;
|
||||||
--good-bg: rgba(74, 210, 139, 0.12);
|
--good-bg: rgba(74, 210, 139, 0.12);
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
--warn-bg: rgba(255, 206, 91, 0.12);
|
--warn-bg: rgba(255, 206, 91, 0.12);
|
||||||
--danger: #ff6f7d;
|
--danger: #ff6f7d;
|
||||||
--danger-bg: rgba(255, 111, 125, 0.12);
|
--danger-bg: rgba(255, 111, 125, 0.12);
|
||||||
--info-bg: rgba(122, 169, 255, 0.12);
|
--info-bg: rgba(11, 149, 199, 0.12);
|
||||||
|
|
||||||
--radius: 10px;
|
--radius: 10px;
|
||||||
--radius-lg: 14px;
|
--radius-lg: 14px;
|
||||||
@@ -107,10 +107,10 @@ button[type="submit"], button.primary {
|
|||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
color: #0a0c10;
|
color: #0a0c10;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: 0 4px 14px rgba(122, 169, 255, 0.25);
|
box-shadow: 0 4px 14px rgba(11, 149, 199, 0.28);
|
||||||
}
|
}
|
||||||
button[type="submit"]:hover, button.primary:hover {
|
button[type="submit"]:hover, button.primary:hover {
|
||||||
box-shadow: 0 6px 20px rgba(122, 169, 255, 0.4);
|
box-shadow: 0 6px 20px rgba(51, 235, 255, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.danger {
|
button.danger {
|
||||||
@@ -158,7 +158,7 @@ input:hover, textarea:hover, select:hover { border-color: #3a425a; }
|
|||||||
input:focus, textarea:focus, select:focus {
|
input:focus, textarea:focus, select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 0 0 3px rgba(122, 169, 255, 0.15);
|
box-shadow: 0 0 0 3px rgba(11, 149, 199, 0.2);
|
||||||
}
|
}
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
@@ -215,6 +215,27 @@ label > select {
|
|||||||
#login p { margin: 0 0 22px; }
|
#login p { margin: 0 0 22px; }
|
||||||
#login button[type="submit"] { width: 100%; padding: 10px; font-size: 14px; }
|
#login button[type="submit"] { width: 100%; padding: 10px; font-size: 14px; }
|
||||||
|
|
||||||
|
.brand-lockup {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.logo-mark {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #0a0c10;
|
||||||
|
border: 1px solid rgba(51, 235, 255, 0.28);
|
||||||
|
box-shadow: 0 6px 18px rgba(11, 149, 199, 0.34);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.logo-mark img { width: 100%; height: 100%; object-fit: contain; padding: 1px; }
|
||||||
|
|
||||||
@keyframes fadeUp {
|
@keyframes fadeUp {
|
||||||
from { opacity: 0; transform: translateY(12px); }
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
@@ -280,7 +301,7 @@ nav .tab:hover { color: var(--text-dim); transform: none; background: var(--pane
|
|||||||
nav .tab.active {
|
nav .tab.active {
|
||||||
background: var(--info-bg);
|
background: var(--info-bg);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border-color: rgba(122, 169, 255, 0.25);
|
border-color: rgba(51, 235, 255, 0.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-panel {
|
.tab-panel {
|
||||||
@@ -382,7 +403,7 @@ nav .tab.active {
|
|||||||
}
|
}
|
||||||
.table tr:last-child td { border-bottom: none; }
|
.table tr:last-child td { border-bottom: none; }
|
||||||
.table tr { transition: background var(--t); }
|
.table tr { transition: background var(--t); }
|
||||||
.table tbody tr:hover td { background: rgba(122, 169, 255, 0.04); }
|
.table tbody tr:hover td { background: rgba(11, 149, 199, 0.06); }
|
||||||
|
|
||||||
.table .actions {
|
.table .actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -419,7 +440,7 @@ nav .tab.active {
|
|||||||
.badge.no-dot::before { display: none; }
|
.badge.no-dot::before { display: none; }
|
||||||
.badge.online { background: var(--good-bg); color: var(--good); border-color: rgba(74, 210, 139, 0.2); }
|
.badge.online { background: var(--good-bg); color: var(--good); border-color: rgba(74, 210, 139, 0.2); }
|
||||||
.badge.offline { background: rgba(139, 145, 158, 0.08); color: var(--muted); border-color: rgba(139, 145, 158, 0.18); }
|
.badge.offline { background: rgba(139, 145, 158, 0.08); color: var(--muted); border-color: rgba(139, 145, 158, 0.18); }
|
||||||
.badge.admin { background: var(--info-bg); color: var(--accent); border-color: rgba(122, 169, 255, 0.2); }
|
.badge.admin { background: var(--info-bg); color: var(--accent); border-color: rgba(51, 235, 255, 0.2); }
|
||||||
.badge.banned { background: var(--danger-bg); color: var(--danger); border-color: rgba(255, 111, 125, 0.2); }
|
.badge.banned { background: var(--danger-bg); color: var(--danger); border-color: rgba(255, 111, 125, 0.2); }
|
||||||
|
|
||||||
.badge-row { display: inline-flex; gap: 4px; flex-wrap: wrap; }
|
.badge-row { display: inline-flex; gap: 4px; flex-wrap: wrap; }
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -3,13 +3,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#0b95c7">
|
||||||
<title>DorkNet — Feed</title>
|
<title>DorkNet — Feed</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="brand/dorknet.svg">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<a href="/" class="logo">
|
<a href="/" class="logo">
|
||||||
<span class="logo-mark">●</span>
|
<span class="logo-mark"><img src="brand/dorknet.svg" alt=""></span>
|
||||||
<span class="logo-text">DorkNet</span>
|
<span class="logo-text">DorkNet</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--bg: #07080b;
|
--bg: #07080b;
|
||||||
--bg-grad: radial-gradient(circle at 20% -10%, #1d1748 0%, #07080b 55%);
|
--bg-grad: radial-gradient(circle at 18% -8%, rgba(11, 149, 199, 0.22) 0%, rgba(7, 8, 11, 0) 42%), linear-gradient(135deg, #07080b 0%, #0c171c 48%, #07080b 100%);
|
||||||
--panel: rgba(20, 22, 30, 0.7);
|
--panel: rgba(20, 22, 30, 0.7);
|
||||||
--panel-2: #161922;
|
--panel-2: #161922;
|
||||||
--panel-3: #1f2330;
|
--panel-3: #1f2330;
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
--text: #f0f2f8;
|
--text: #f0f2f8;
|
||||||
--text-dim: #b0b6c8;
|
--text-dim: #b0b6c8;
|
||||||
--muted: #757b8e;
|
--muted: #757b8e;
|
||||||
--accent: #ff6ec7;
|
--accent: #33ebff;
|
||||||
--accent-2: #7aa9ff;
|
--accent-2: #0b95c7;
|
||||||
--accent-grad: linear-gradient(135deg, #ff6ec7 0%, #b48cff 50%, #7aa9ff 100%);
|
--accent-grad: linear-gradient(135deg, #33ebff 0%, #0b95c7 100%);
|
||||||
--good: #4ad28b;
|
--good: #4ad28b;
|
||||||
--danger: #ff6f7d;
|
--danger: #ff6f7d;
|
||||||
--shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
--shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
||||||
@@ -41,7 +41,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a { color: var(--accent-2); text-decoration: none; transition: color var(--t); }
|
a { color: var(--accent-2); text-decoration: none; transition: color var(--t); }
|
||||||
a:hover { color: #a3c4ff; }
|
a:hover { color: #c8f3ff; }
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background: var(--panel-2);
|
background: var(--panel-2);
|
||||||
@@ -61,15 +61,15 @@ button.primary {
|
|||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
color: #07080b;
|
color: #07080b;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: 0 4px 18px rgba(255, 110, 199, 0.25);
|
box-shadow: 0 4px 18px rgba(11, 149, 199, 0.28);
|
||||||
}
|
}
|
||||||
button.primary:hover { box-shadow: 0 6px 24px rgba(255, 110, 199, 0.4); }
|
button.primary:hover { box-shadow: 0 6px 24px rgba(51, 235, 255, 0.28); }
|
||||||
button.ghost {
|
button.ghost {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
button.ghost:hover { color: var(--text); border-color: var(--accent); background: rgba(255,110,199,0.06); }
|
button.ghost:hover { color: var(--text); border-color: var(--accent); background: rgba(11, 149, 199, 0.1); }
|
||||||
|
|
||||||
/* ── Site shell ──────────────────────────────────────────────────── */
|
/* ── Site shell ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -97,13 +97,19 @@ button.ghost:hover { color: var(--text); border-color: var(--accent); background
|
|||||||
}
|
}
|
||||||
.logo:hover { color: var(--text); }
|
.logo:hover { color: var(--text); }
|
||||||
.logo-mark {
|
.logo-mark {
|
||||||
width: 22px;
|
width: 28px;
|
||||||
height: 22px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 8px;
|
||||||
background: var(--accent-grad);
|
background: #07080b;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
box-shadow: 0 4px 14px rgba(255, 110, 199, 0.5);
|
box-shadow: 0 4px 14px rgba(11, 149, 199, 0.42);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid rgba(51, 235, 255, 0.28);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.logo-mark img { width: 100%; height: 100%; object-fit: contain; padding: 1px; }
|
||||||
.site-nav {
|
.site-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
|
||||||
|
<rect id="Tavola-da-disegno1" serif:id="Tavola da disegno1" x="0" y="0" width="1080" height="1080" style="fill:none;"/>
|
||||||
|
<g id="Tavola-da-disegno11" serif:id="Tavola da disegno1">
|
||||||
|
<g transform="matrix(0.966367,-1.703882,1.703882,0.966367,-124.38412,723.826641)">
|
||||||
|
<ellipse cx="249.71" cy="250.06" rx="121.23" ry="247.82" style="fill:rgb(200,243,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip2">
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip2)">
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M383.298,380.419C359.792,373.449 334.386,368.102 300.108,372.736C272.966,376.405 245.845,385.565 231.091,390.596L225.318,373.463C259.192,362.43 295.348,352.411 333.238,354.558C351.669,355.602 369.398,359.365 388.465,365.398C417.174,374.482 403.887,387.691 418.365,412.449C434.713,420.625 486.506,448.447 514.251,516.006C528.415,550.494 533.264,588.31 529.151,624.638C522.661,681.958 496.254,716.526 489.48,725.417L481.335,719.228C486.524,712.173 516.533,673.169 518.45,608.869C520.6,536.765 485.888,461.595 411.148,425.724C385.618,413.471 396.109,403.478 383.298,380.419Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-40.206636,-65.042535)">
|
||||||
|
<path d="M516.389,742.353L510.331,737.233C517.614,728.283 552.833,686.488 553.815,611.549C554.808,535.791 519.635,452.341 449.375,402.174C448.959,401.877 448.998,401.831 444.275,398.327L452.449,386.395C457.784,389.848 457.811,389.811 462.829,393.709C572.192,478.66 597.626,646.024 517.567,740.97L516.389,742.353Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.564731,0,0,0.564731,-23.866246,-81.382925)">
|
||||||
|
<path d="M533.438,757.673L526.891,753.57C536.596,737.676 570.912,680.138 559.158,593.709C541.725,465.513 432.263,349.713 295.811,357.465L294.658,357.532L293.758,339.475C314.259,338.685 385.515,336.865 457.769,392.943C564.058,475.436 610.007,636.274 534.39,756.175C533.916,756.927 533.915,756.923 533.438,757.673Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M308.29,210.37C279.19,136 183.21,43 29.77,130.68L55.77,195.93C63.327,191.377 71.22,187.406 79.38,184.05L140.38,362.51C128.31,362.68 100.64,360.65 90.49,351.96C79.2,342.3 114.81,394.52 200.79,393.67C299.75,392.7 340.79,293.43 308.29,210.37ZM197.28,345.34L149.71,171.16C179.29,173.83 209.71,189.16 230.89,231.7C259.4,289 231.81,325.79 197.28,345.34Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,53.870869,50.218665)">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
|
||||||
|
<clipPath id="_clip4">
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" clip-rule="nonzero"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip4)">
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M719.298,286.117L719.472,302.116C714.034,302.198 680.645,301.87 647.172,333.748C618.509,361.044 599.916,402.886 602.893,445.447C605.339,480.414 612.06,542.816 672.087,560.368C704.064,569.718 738.525,562.673 746.775,561.646C798.125,555.255 863.977,582.481 844.976,672.066C826.557,758.904 708.786,793.434 604.896,734.778C603.991,734.267 603.996,734.265 603.094,733.751L611.06,719.875C619.352,724.604 666.616,752.472 726.054,747.259C776.183,742.862 819.57,715.532 829.427,668.257C846.074,588.41 787.093,574.077 752.204,577.157C744.709,577.819 735.843,579.959 719.66,580.846C610.634,586.829 593.432,503.624 588.863,467.303C588.402,463.633 586.629,448.059 586.581,437.107C586.387,392.666 606.6,349.614 637.435,320.939C674.957,286.046 712.599,286.218 719.298,286.117Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.510505,0,0,0.510505,-25.969815,-25.636862)">
|
||||||
|
<path d="M889.582,670.145C886.387,692.654 874.229,756.889 799.538,791.868C768.431,806.436 738.412,810.153 727.3,811.484C726.145,801.395 716.291,796.674 726.378,795.483C798.043,787.023 862.555,749.781 873.868,667.055C880.827,616.172 906.947,635.6 944.161,600.212L954.792,612.169L889.582,670.145Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.48417,0,0,0.48417,290.849928,159.731518)">
|
||||||
|
<path d="M-30.174,347.361C-30.091,347.362 -16.814,346.654 -7.19,358.034C5.418,372.943 0.646,395.741 -16.795,404.414C-33.634,412.787 -53.997,403.726 -59.125,385.688C-64.446,366.968 -50.777,348.122 -31.331,347.382C-31.239,347.379 -31.239,347.381 -30.174,347.361ZM-30.173,366.335C-30.269,366.339 -30.366,366.341 -30.465,366.344C-38.677,366.537 -44.049,375.524 -39.822,382.976C-35.513,390.572 -24.064,390.508 -20.093,382.142C-16.849,375.307 -21.5,366.447 -30.173,366.34C-30.173,366.338 -30.173,366.337 -30.173,366.335ZM-30.173,366.34C-30.173,366.36 -30.254,366.339 -30.234,366.34C-30.214,366.34 -30.193,366.34 -30.173,366.34Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.409654,0,0,0.409654,348.629981,229.899333)">
|
||||||
|
<path d="M-30.174,346.881C-30.091,346.882 -16.6,346.163 -6.823,357.724C6.389,373.348 0.742,397.3 -17.929,405.473C-36.437,413.575 -57.656,401.68 -60.452,381.718C-61.661,373.092 -59.166,364.231 -53.287,357.446C-43.552,346.209 -30.382,346.885 -30.174,346.881ZM-30.179,348.606C-30.272,365.541 -31.682,366.793 -30.237,366.819C-30.24,366.819 -30.241,366.82 -30.239,366.821C-40.09,366.999 -44.296,378.834 -37.414,385.24C-30.923,391.283 -20.188,387.144 -19.561,378.146C-19.192,372.846 -23.119,366.95 -30.173,366.82C-30.144,357.461 -30.198,356.892 -30.179,348.606ZM-30.173,366.82C-30.173,366.837 -30.23,366.826 -30.239,366.821C-30.218,366.821 -30.197,366.82 -30.176,366.82C-30.175,366.82 -30.174,366.82 -30.173,366.82Z" style="fill:rgb(51,235,255);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path d="M392.19,205.85L452.51,182C452.51,182 439.38,111.36 360.12,103.73C280.86,96.1 255,167.94 254.8,195C254.6,222.06 267.09,299.93 339.53,284.5C372.33,277.5 385.19,290.19 383.29,309.2C380.98,332.28 336.11,370.64 268.29,309.65L237,336.56C237,336.56 262,403.62 334.06,403.68C399.06,403.73 457.8,364.68 446.68,289.25C441.03,250.9 407.51,217.39 357.62,221.89C300.62,227.03 323.13,172.89 355.12,171.94C394.19,170.77 392.19,205.85 392.19,205.85Z" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:5px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M125.81,223.01L161.59,281.96L234.69,250.76L228.53,178.91" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M250.31,171.25L297.85,221.85L363.95,190.85L351.12,130.21" style="fill:none;fill-rule:nonzero;stroke:rgb(125,132,143);stroke-width:9px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.958846,0,0,1.958846,104.702924,223.752836)">
|
||||||
|
<path d="M76.64,234.99L78.95,211.91L122.32,223.62L133.9,209.26L204.54,179.63L228.56,179.91L250.31,171.25L264.34,155.33L331.44,124.3L347.49,125.82L335.99,92.65" style="fill:none;fill-rule:nonzero;stroke:rgb(26,26,26);stroke-width:13px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1,0,0,1,237.128515,-26.039343)">
|
||||||
|
<path d="M-30.174,346.85C-30.091,346.852 -16.587,346.132 -6.8,357.704C5.921,372.747 1.262,395.736 -16.236,404.704C-32.978,413.284 -53.484,404.767 -59.286,386.911C-65.4,368.095 -52.217,348.44 -32.518,346.939C-31.347,346.85 -31.349,346.872 -30.174,346.85ZM-30.174,366.848C-30.745,366.832 -35.068,366.825 -38.277,370.619C-43.539,376.841 -40.028,386.366 -32.189,387.87C-23.778,389.483 -17.066,380.949 -20.45,373.218C-21.243,371.406 -22.544,369.843 -24.151,368.729C-26.865,366.847 -29.534,366.855 -30.174,366.85L-30.112,366.85C-30.116,366.85 -30.137,366.849 -30.174,366.848Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.868104,0,0,0.868104,340.346177,-48.424243)">
|
||||||
|
<path d="M-30.174,346.515C-30.091,346.517 -16.438,345.789 -6.544,357.488C6.621,373.056 1.318,396.894 -17.096,405.497C-35.045,413.883 -56.201,403.351 -60.405,384.057C-64.507,365.238 -50.552,347.268 -31.363,346.538C-31.268,346.534 -31.269,346.536 -30.174,346.515ZM-30.175,356.504L-30.176,367.186C-30.197,367.185 -30.218,367.185 -30.239,367.185C-30.243,367.185 -30.243,367.186 -30.241,367.187C-39.761,367.366 -43.804,378.792 -37.165,384.972C-30.855,390.847 -20.418,386.751 -19.918,377.991C-19.631,372.952 -23.381,367.313 -30.173,367.186C-30.156,361.786 -30.166,359.208 -30.175,356.504ZM-30.173,367.186C-30.173,367.204 -30.232,367.191 -30.241,367.187C-30.219,367.186 -30.198,367.186 -30.176,367.186C-30.175,367.186 -30.174,367.186 -30.173,367.186Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.736248,0,0,0.736248,483.945338,73.515167)">
|
||||||
|
<path d="M-30.174,345.984C-30.091,345.986 -16.202,345.245 -6.139,357.145C7.565,373.35 1.541,398.208 -17.922,406.446C-37.518,414.741 -59.637,401.448 -61.521,380.263C-62.303,371.462 -59.382,362.805 -53.465,356.295C-43.544,345.381 -30.619,345.993 -30.174,345.984ZM-30.175,356.435L-30.176,367.717C-30.198,367.716 -30.22,367.716 -30.242,367.716C-30.246,367.716 -30.247,367.717 -30.245,367.718C-39.336,367.901 -43.117,378.846 -36.717,384.664C-30.745,390.092 -21.176,386.235 -20.475,378.338C-20.018,373.2 -23.745,367.842 -30.173,367.717C-30.155,362.006 -30.166,359.296 -30.175,356.435ZM-30.173,367.717C-30.173,367.735 -30.235,367.723 -30.245,367.718C-30.222,367.717 -30.199,367.717 -30.176,367.717C-30.175,367.717 -30.173,367.717 -30.173,367.717Z" style="fill:rgb(11,149,199);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(97.382232,67.814092,-67.814092,97.382232,151.218217,203.626226)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.53364,107.893491,-162.475262,-5.321257,318.003313,232.433991)"><stop offset="0" style="stop-color:rgb(51,235,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(11,149,199);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.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>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Commerce);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/commerce/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Commerce,
|
||||||
|
[
|
||||||
|
"catalog",
|
||||||
|
"storefronts",
|
||||||
|
"econ",
|
||||||
|
"inventory",
|
||||||
|
"inventions",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/api/catalog/*",
|
||||||
|
"/api/storefronts/*",
|
||||||
|
"/econ/*",
|
||||||
|
"/api/equipment/*",
|
||||||
|
"/api/inventions/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Commerce);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Content);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/content/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Content,
|
||||||
|
[
|
||||||
|
"cdn",
|
||||||
|
"images",
|
||||||
|
"photos",
|
||||||
|
"room-blobs",
|
||||||
|
"storage",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/api/images/*",
|
||||||
|
"/api/photos/*",
|
||||||
|
"/data/*",
|
||||||
|
"/img/*",
|
||||||
|
"/upload",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Content);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Identity);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/identity/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Identity,
|
||||||
|
[
|
||||||
|
"accounts",
|
||||||
|
"auth",
|
||||||
|
"platform-login",
|
||||||
|
"jwt-issuance",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/account/*",
|
||||||
|
"/api/account/*",
|
||||||
|
"/api/platformlogin/*",
|
||||||
|
"/api/auth/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Identity);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Moderation);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/moderation/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Moderation,
|
||||||
|
[
|
||||||
|
"bug-reporting",
|
||||||
|
"player-reporting",
|
||||||
|
"sanitize",
|
||||||
|
"admin-api",
|
||||||
|
"testcase-management",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/api/bugreporting/*",
|
||||||
|
"/api/playerreporting/*",
|
||||||
|
"/api/sanitize/*",
|
||||||
|
"/api/admin/*",
|
||||||
|
"/api/testcasemanagement/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Moderation);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Notify);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/notify/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Notify,
|
||||||
|
[
|
||||||
|
"signalr-edge",
|
||||||
|
"notification-fanout",
|
||||||
|
"presence",
|
||||||
|
"player-request-logs",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/hub/v1",
|
||||||
|
"/api/notification/*",
|
||||||
|
"/player/*presence*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Notify);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Platform);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/platform/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Platform,
|
||||||
|
[
|
||||||
|
"service-directory",
|
||||||
|
"config",
|
||||||
|
"version-check",
|
||||||
|
"geo",
|
||||||
|
"strings",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/v1/services",
|
||||||
|
"/api/config/*",
|
||||||
|
"/api/versioncheck/*",
|
||||||
|
"/v1/regions",
|
||||||
|
"/strings/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Platform);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Rooms);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/rooms/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Rooms,
|
||||||
|
[
|
||||||
|
"rooms",
|
||||||
|
"subrooms",
|
||||||
|
"room-keys",
|
||||||
|
"matchmaking",
|
||||||
|
"discovery",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/rooms/*",
|
||||||
|
"/api/rooms/*",
|
||||||
|
"/roomserver/*",
|
||||||
|
"/v1/find",
|
||||||
|
"/v1/join/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Rooms);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Social);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/social/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Social,
|
||||||
|
[
|
||||||
|
"clubs",
|
||||||
|
"groups",
|
||||||
|
"announcements",
|
||||||
|
"player-events",
|
||||||
|
"subscriptions",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/club/*",
|
||||||
|
"/api/groups/*",
|
||||||
|
"/api/playerevents/*",
|
||||||
|
"/api/playersubscriptions/*",
|
||||||
|
"/announcements/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Social);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DorkNet.Contracts\DorkNet.Contracts.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.ServiceDefaults\DorkNet.ServiceDefaults.csproj" />
|
||||||
|
<ProjectReference Include="..\DorkNet.Server\DorkNet.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<DorkNetServerWebAsset Include="..\DorkNet.Server\wwwroot\**\*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="CopyDorkNetServerWebAssets"
|
||||||
|
AfterTargets="Publish"
|
||||||
|
Condition="'$(DorkNetServiceContainerPublish)' == 'true'">
|
||||||
|
<Copy SourceFiles="@(DorkNetServerWebAsset)"
|
||||||
|
DestinationFiles="@(DorkNetServerWebAsset->'$(PublishDir)wwwroot/%(RecursiveDir)%(Filename)%(Extension)')"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using DorkNet.Contracts;
|
||||||
|
using DorkNet.ServiceDefaults;
|
||||||
|
using DorkNet.Server.Startup;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.Local.json",
|
||||||
|
optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.AddDorkNetServiceDefaults(ServiceNames.Web);
|
||||||
|
builder.AddDorkNetServices();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
await app.RunDatabaseBootstrapAsync();
|
||||||
|
app.MapDorkNetServiceDefaults(mapPublicHealth: false);
|
||||||
|
app.MapGet("/internal/web/capabilities", () =>
|
||||||
|
Results.Ok(new ServiceCapabilityResponse(
|
||||||
|
ServiceNames.Web,
|
||||||
|
[
|
||||||
|
"public-site",
|
||||||
|
"admin-site",
|
||||||
|
"feed-site",
|
||||||
|
"site-api",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/",
|
||||||
|
"www.*",
|
||||||
|
"admin.*",
|
||||||
|
"feed.*",
|
||||||
|
"/api/site/*",
|
||||||
|
])));
|
||||||
|
|
||||||
|
app.UseDorkNetRouteOwnershipGuard(ServiceNames.Web);
|
||||||
|
app.UseDorkNetPipeline();
|
||||||
|
|
||||||
|
app.Run();
|
||||||