Files
DorkNet/DorkNet.Server/DorkNet.Server.csproj
Alexa f0e4679887 feat(auth): external sign-in for the website and admin panel
Players sign in to the website, and admins to the admin panel, with an
OpenID Connect provider (DorkNet:SignIn:*). The first sign-in links the
provider identity (issuer + subject) to a DorkNet account with that
account's password; after that the provider sign-in is enough.

- Cookie sessions per host, keys persisted in the database so redeploys
  keep people signed in; unlink, deletion or a ban ends the session.
- Link attempts limited to 5 failures per 15 minutes per identity and
  per username.
- Admin panel: "Sign in with ..." exchanges the session for the usual
  admin tokens via GET /api/admin/v1/sso (IsAdmin still required).
- Downloads: optional DorkNet:Downloads:RequireSignIn serves the
  manifest and files only to signed-in players; the Downloads and Quest
  pages offer sign-in instead.
- Website: sign-in in the header, /link and /account pages.

The game client's login is unchanged.
2026-10-25 15:47:56 +02:00

177 lines
10 KiB
XML

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.13" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="10.0.13" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.13">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.13" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<!-- RenderedCompactJsonFormatter for stdout JSON logs in production
(Coolify-friendly). The host pipeline reads stdout line-by-line,
and structured JSON lets a future Loki/Datadog ingest parse fields
without regex. Local dev still gets human-readable output via the
default text formatter (configured by environment in Program.cs). -->
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<!-- Redis backplane + ephemeral state store. Required for horizontal
scale: SignalR group fanout, OrphanAccountTracker (sticky-session
account-creation flow), and PlayerPresenceService heartbeats all
need shared state across replicas. -->
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="10.0.13" />
<!-- S3-compatible object storage. Production target: Garage in
Coolify (one-click). Dev fallback: local MinIO for parity
testing. The SDK speaks plain S3, so swapping endpoints
(Garage → R2 → real AWS) is a config change, not a code
change. ForcePathStyle=true is required for both Garage and
MinIO since neither implements virtual-host-style addressing. -->
<PackageReference Include="AWSSDK.S3" Version="3.7.412.5" />
<!-- Password hashing for the real account auth flow. BCrypt embeds
cost+salt in the output string so we only need one column. -->
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.3.1" />
<!-- Server-side crop/resize for img.* CDN transforms (?cropSquare=1&width=N
on profile thumbnails). The 2020 client treats this as part of the
image CDN contract — it uploads a full-frame capture and expects the
server to return a center-cropped, downsized version. SkiaSharp ships
the native Skia binary per RID; the runtime package below pulls in
Linux+Windows so Docker publishes correctly. -->
<PackageReference Include="SkiaSharp" Version="3.116.1" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.116.1" />
<!-- Protobuf encoding for the room data blob (PersistedRoomData → granted maker pen). -->
<PackageReference Include="Google.Protobuf" Version="3.28.3" />
<PackageReference Include="Grpc.Tools" Version="2.66.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<!-- Compiles the trimmed proto schema in Protos/ to C# at build time.
Only what we need to encode a minimum-viable PersistedRoomData. -->
<Protobuf Include="Protos\dorknet_room_data.proto" GrpcServices="None" />
<!-- Full 2020.03.06 PersistedRoomData schema, generated from
Cpp2IL_CS/.../RecRoom/Protobuf/*.cs by
tools/gen-proto-from-decompiled.py. csharp_namespace =
RecRoom.Protobuf2020 (so the small trimmed schema in the
file above can keep using RecRoom.Protobuf.PersistedRoomData
without colliding). RoomBlobNormalizerService uses the
generated PersistedRoomData here to ParseFrom + ToByteArray
any uploaded room blob, normalising newer-format encodings
that the watch's stricter 2020 parser rejects. -->
<Protobuf Include="Protos\recroom_2020.proto" GrpcServices="None" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DorkNet.Models\DorkNet.Models.csproj" />
</ItemGroup>
<!-- Service-slice containers reference DorkNet.Server for shared controllers
and services, but each slice owns its own appsettings.json. Without this
guard, dotnet publish sees both files targeting appsettings.json in the
same publish root and fails NETSDK1152. -->
<ItemGroup Condition="'$(DorkNetServiceContainerPublish)' == 'true'">
<Content Update="appsettings*.json" CopyToPublishDirectory="Never" />
<None Update="appsettings*.json" CopyToPublishDirectory="Never" />
</ItemGroup>
<!-- Vite admin SPA build. Source at admin-ui/, output goes to
wwwroot/admin/ which the per-host static file middleware
already serves on admin.rec.net / admin.localhost.
BeforeTargets=Publish so `dotnet publish` always rebuilds the
SPA into the publish output. Local `dotnet build` skips this
(use `npm run build` from admin-ui/ directly while iterating
on the SPA), keeping the .NET build fast.
SkipAdminUIBuild=true bypasses this entirely — used by the
Dockerfile, which builds the SPA in a dedicated node:* stage
and copies the dist into wwwroot/admin/ before `dotnet publish`
runs (the .NET SDK image doesn't ship npm). -->
<Target Name="BuildAdminUI" BeforeTargets="Publish"
Condition="Exists('admin-ui\package.json') AND '$(SkipAdminUIBuild)' != 'true'">
<Message Text="[admin-ui] npm ci + npm run build" Importance="high" />
<Exec Command="npm ci --no-fund --no-audit --loglevel=error" WorkingDirectory="admin-ui" />
<Exec Command="npm run build" WorkingDirectory="admin-ui" />
</Target>
<!-- Public-facing site (localhost / rec.net). Same shape as the
admin-ui target above — separate Vite project under site/,
output into wwwroot/site/. Dockerfile builds this in its own
node stage and sets SkipSiteBuild=true so this no-ops there. -->
<Target Name="BuildSite" BeforeTargets="Publish"
Condition="Exists('site\package.json') AND '$(SkipAdminUIBuild)' != 'true'">
<Message Text="[site] npm ci + npm run build" Importance="high" />
<Exec Command="npm ci --no-fund --no-audit --loglevel=error" WorkingDirectory="site" />
<Exec Command="npm run build" WorkingDirectory="site" />
</Target>
<!-- Web SDK auto-includes Content items from the project tree, but
defaults to CopyToOutputDirectory=Never, AND only auto-includes
a fixed list of extensions. CDN-hash-named PNGs sourced from
tools/fetch-room-images.py (e.g. 50xrbhogfdae0mcgzfkomlghu.png)
end up in Data\images\ on a case-insensitive filesystem but the
<Content Update="data\..."> rule, lowercased to match the
runtime path the loader looks under, doesn't touch them.
Resort to <None Include> with a forced output path: None+CopyToOutputDirectory
reliably copies regardless of the auto-Content set, and using
<Link> places them under the lowercase data\images\ that
AppContext.BaseDirectory + "data/images" resolves to at
runtime in the merged CdnController (img.* branch). -->
<ItemGroup>
<Content Update="data\**\*.json" CopyToOutputDirectory="PreserveNewest" />
<Content Update="data\images\**\*.png" CopyToOutputDirectory="PreserveNewest" />
<Content Update="data\images\**\*.jpg" CopyToOutputDirectory="PreserveNewest" />
<!-- Top-level data/*.json (avatar_item_lookup.json, room_images.json,
etc.) — the source files are tracked at <c>DorkNet.Server/Data/</c>
(uppercase) in git but the runtime loaders look in the lowercase
<c>data/</c> dir. On Windows (case-insensitive fs) the
<c>Content Update="data\**\*.json"</c> above silently matches the
uppercase tree; on the Linux build container (case-sensitive) it
does NOT, so the JSONs are skipped during dotnet publish and
AvatarItemsController loads an empty catalog (which collapses
the stale-row cleanup in StoreService.SeedAsync and leaves
hundreds of swatch-GUID rows live → every store tile substitutes
to Hat_Angler). Force-copy via <None>+<Link> to the lowercase
path so both filesystems see the file at runtime.
IMPORTANT: keep the Exclude — Data\images\ has its own copy rule
above for PNG/JPG, and a stray *.json in there (none today, but
defence in depth) would otherwise get linked into the wrong
place. -->
<None Include="Data\*.json">
<Link>data\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<!-- Real captured PersistedRoomData served as the default room blob
(RoomDataBlobService.GetDefaultBlob). The hand-built synthetic blob
is too thin for the 2023 client's room-load apply path. -->
<None Include="Data\*.room">
<Link>data\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="Data\images\*.png" Exclude="Data\images\image_*.png">
<Link>data\images\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="Data\images\image_*.png">
<Link>data\images\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="Data\images\*.jpg">
<Link>data\images\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>