diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..9e03c48
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,32 @@
+# Include any files or directories that you don't want to be copied to your
+# container here (e.g., local build artifacts, temporary files, etc.).
+#
+# For more help, visit the .dockerignore file reference guide at
+# https://docs.docker.com/go/build-context-dockerignore/
+
+**/.DS_Store
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/bin
+**/charts
+**/docker-compose*
+**/compose.y*ml
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
diff --git a/.gitignore b/.gitignore
index 026b94f..c05f5a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -362,4 +362,4 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
-appsettings.production.json
\ No newline at end of file
+Certs
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..6a2da11
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,56 @@
+# syntax=docker/dockerfile:1
+
+# Comments are provided throughout this file to help you get started.
+# If you need more help, visit the Dockerfile reference guide at
+# https://docs.docker.com/go/dockerfile-reference/
+
+# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7
+
+################################################################################
+
+# Learn about building .NET container images:
+# https://github.com/dotnet/dotnet-docker/blob/main/samples/README.md
+
+# Create a stage for building the application.
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
+
+COPY . /source
+
+WORKDIR /source/PurrLobby
+
+# This is the architecture you’re building for, which is passed in by the builder.
+# Placing it here allows the previous steps to be cached across architectures.
+ARG TARGETARCH
+
+# Build the application.
+# Leverage a cache mount to /root/.nuget/packages so that subsequent builds don't have to re-download packages.
+# If TARGETARCH is "amd64", replace it with "x64" - "x64" is .NET's canonical name for this and "amd64" doesn't
+# work in .NET 6.0.
+RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
+ dotnet publish -a ${TARGETARCH/amd64/x64} --use-current-runtime --self-contained false -o /app
+
+# If you need to enable globalization and time zones:
+# https://github.com/dotnet/dotnet-docker/blob/main/samples/enable-globalization.md
+################################################################################
+# Create a new stage for running the application that contains the minimal
+# runtime dependencies for the application. This often uses a different base
+# image from the build stage where the necessary files are copied from the build
+# stage.
+#
+# The example below uses an aspnet alpine image as the foundation for running the app.
+# It will also use whatever happens to be the most recent version of that tag when you
+# build your Dockerfile. If reproducibility is important, consider using a more specific
+# version (e.g., aspnet:7.0.10-alpine-3.18),
+# or SHA (e.g., mcr.microsoft.com/dotnet/aspnet@sha256:f3d99f54d504a21d38e4cc2f13ff47d67235efeeb85c109d3d1ff1808b38d034).
+FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS final
+WORKDIR /app
+
+# Copy everything needed to run the app from the "build" stage.
+COPY --from=build /app .
+
+# Switch to a non-privileged user (defined in the base image) that the app will run under.
+# See https://docs.docker.com/go/dockerfile-user-best-practices/
+# and https://github.com/dotnet/dotnet-docker/discussions/4764
+USER $APP_UID
+
+ENTRYPOINT ["dotnet", "PurrLobby.dll"]
diff --git a/PurrLobby/Models/Lobby.cs b/PurrLobby/Models/Lobby.cs
index 6c36ad6..975cebd 100644
--- a/PurrLobby/Models/Lobby.cs
+++ b/PurrLobby/Models/Lobby.cs
@@ -5,6 +5,7 @@ public class LobbyUser
{
public required string Id { get; init; }
public required string DisplayName { get; init; }
+ public int userPing { get; set; }
public bool IsReady { get; set; }
}
diff --git a/PurrLobby/Pages/Index.cshtml b/PurrLobby/Pages/Index.cshtml
new file mode 100644
index 0000000..edd78f5
--- /dev/null
+++ b/PurrLobby/Pages/Index.cshtml
@@ -0,0 +1,90 @@
+@page
+@model PurrLobby.Pages.IndexModel
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+
+
+
+ This Service does not (yet) belong to purrnet, it's just something I put together for an alternative to the Steam Lobby Service and Unity Service.
+ Since it's running on my own setup, it might go down or act weird sometimes.
+ If that happens, no worries, it'll probably be back soon.
+ Please don't contact the actual PurrNet devs about this, it's all on me.
+ If there are any problems contact me on discord: exil_s
+
+
+
+
diff --git a/PurrLobby/Pages/Index.cshtml.cs b/PurrLobby/Pages/Index.cshtml.cs
new file mode 100644
index 0000000..716bac2
--- /dev/null
+++ b/PurrLobby/Pages/Index.cshtml.cs
@@ -0,0 +1,91 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace PurrLobby.Pages
+{
+ public class IndexModel : PageModel
+ {
+ [BindProperty]
+ public string GameIdInput { get; set; } = string.Empty;
+ public string CookieStatus { get; set; } = string.Empty;
+ public int GlobalPlayers { get; set; } = 0;
+ public int GlobalLobbies { get; set; } = 0;
+ public int GamePlayers { get; set; } = 0;
+ public int GameLobbies { get; set; } = 0;
+
+ public async Task OnGetAsync()
+ {
+ await LoadStatsAsync();
+ }
+
+ public async Task OnPostAsync()
+ {
+ if (!Guid.TryParse(GameIdInput, out var gameId))
+ {
+ CookieStatus = "Invalid Game ID format.";
+ await LoadStatsAsync();
+ return Page();
+ }
+ // Set gameId cookie via backend API
+ try
+ {
+ using var client = new HttpClient();
+ var response = await client.PostAsync($"{Request.Scheme}://{Request.Host}/session/game", new StringContent(JsonSerializer.Serialize(new { GameId = gameId }), System.Text.Encoding.UTF8, "application/json"));
+ if (response.IsSuccessStatusCode)
+ {
+ CookieStatus = "Game ID set!";
+ }
+ else
+ {
+ CookieStatus = "Failed to set Game ID.";
+ }
+ }
+ catch
+ {
+ CookieStatus = "Error contacting backend.";
+ }
+ await LoadStatsAsync(gameId);
+ return Page();
+ }
+
+ private async Task LoadStatsAsync(Guid? gameId = null)
+ {
+ using var client = new HttpClient();
+ try
+ {
+ var globalPlayersResp = await client.GetStringAsync($"{Request.Scheme}://{Request.Host}/stats/global/players");
+ GlobalPlayers = int.TryParse(globalPlayersResp, out var gp) ? gp : 0;
+ }
+ catch { GlobalPlayers = 0; }
+ try
+ {
+ var globalLobbiesResp = await client.GetStringAsync($"{Request.Scheme}://{Request.Host}/stats/global/lobbies");
+ GlobalLobbies = int.TryParse(globalLobbiesResp, out var gl) ? gl : 0;
+ }
+ catch { GlobalLobbies = 0; }
+ if (gameId.HasValue)
+ {
+ try
+ {
+ var gamePlayersResp = await client.GetStringAsync($"{Request.Scheme}://{Request.Host}/stats/{gameId}/players");
+ GamePlayers = int.TryParse(gamePlayersResp, out var gp) ? gp : 0;
+ }
+ catch { GamePlayers = 0; }
+ try
+ {
+ var gameLobbiesResp = await client.GetStringAsync($"{Request.Scheme}://{Request.Host}/stats/{gameId}/lobbies");
+ GameLobbies = int.TryParse(gameLobbiesResp, out var gl) ? gl : 0;
+ }
+ catch { GameLobbies = 0; }
+ }
+ else
+ {
+ GamePlayers = 0;
+ GameLobbies = 0;
+ }
+ }
+ }
+}
diff --git a/PurrLobby/Program.cs b/PurrLobby/Program.cs
index 23200d8..b76f1d4 100644
--- a/PurrLobby/Program.cs
+++ b/PurrLobby/Program.cs
@@ -86,6 +86,7 @@ builder.Services.AddSwaggerGen(o =>
});
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+builder.Services.AddRazorPages(); // Register Razor Pages services
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
@@ -102,10 +103,10 @@ app.UseResponseCompression();
app.UseHttpLogging();
app.UseRateLimiter();
app.UseWebSockets();
-app.UseDefaultFiles();
app.UseStaticFiles();
app.UseSwagger();
app.UseSwaggerUI();
+app.MapRazorPages(); // Enable Razor Pages
static CookieOptions BuildStdCookieOptions() => new()
{
HttpOnly = true,
@@ -506,4 +507,4 @@ public record CreateLobbyRequest(string OwnerDisplayName, int MaxPlayers, Dictio
public record JoinLobbyRequest(string DisplayName);
public record ReadyRequest(string UserId, bool IsReady);
public record AllReadyRequest(string UserId);
-public record LobbyDataRequest(string Key, string Value);
+public record LobbyDataRequest(string Key, string Value);
\ No newline at end of file
diff --git a/PurrLobby/PurrLobby.csproj b/PurrLobby/PurrLobby.csproj
index 819bfa0..5aa6247 100644
--- a/PurrLobby/PurrLobby.csproj
+++ b/PurrLobby/PurrLobby.csproj
@@ -26,4 +26,14 @@
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
diff --git a/PurrLobby/appsettings.Production.json b/PurrLobby/appsettings.Production.json
deleted file mode 100644
index ef912cb..0000000
--- a/PurrLobby/appsettings.Production.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "Kestrel": {
- "Endpoints": {
- "Https": {
- "Url": "https://0.0.0.0:443",
- "Certificate": {
- "Path": "pemfile",
- "KeyPath": "keyfile"
- }
- }
- }
- },
- "AllowedHosts": "your.domain.com"
-}
diff --git a/PurrLobby/appsettings.json b/PurrLobby/appsettings.json
new file mode 100644
index 0000000..5c63240
--- /dev/null
+++ b/PurrLobby/appsettings.json
@@ -0,0 +1,20 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "purrlobby.exil.dev",
+ "Kestrel": {
+ "Endpoints": {
+ "Https": {
+ "Url": "https://0.0.0.0:443",
+ "Certificate": {
+ "Path": "Certs/purrlobby.pem",
+ "KeyPath": "Certs/purrlobby.key"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PurrLobby/wwwroot/index.html b/PurrLobby/wwwroot/index.html
deleted file mode 100644
index 60914e8..0000000
--- a/PurrLobby/wwwroot/index.html
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
- PurrLobby
-
-
-
-
-
- This Service is does not (yet) belong to purrnet, it's just something I put together for a alternative to the Steam Lobby Service and Unity Service.
- Since it's running on my own setup, it might go down or act weird sometimes.
- If that happens, no worries, it'll probably be back soon.
- Please don't contact the actual PurrNet devs about this, it's all on me.
- If there are any proplem's contact me on discord: exil_s
-
-
-
-
-
-
diff --git a/PurrLobby/wwwroot/main.js b/PurrLobby/wwwroot/main.js
deleted file mode 100644
index d150578..0000000
--- a/PurrLobby/wwwroot/main.js
+++ /dev/null
@@ -1,80 +0,0 @@
-async function fetchJson(url) {
- const r = await fetch(url, { credentials: 'include' });
- if (!r.ok) throw new Error(`Request failed: ${r.status}`);
- return r.json();
-}
-
-async function loadGlobal() {
- try {
- const [players, lobbies] = await Promise.all([
- fetchJson('/stats/global/players'),
- fetchJson('/stats/global/lobbies')
- ]);
- document.getElementById('globalPlayers').textContent = players;
- document.getElementById('globalLobbies').textContent = lobbies;
- } catch (e) {
- console.error(e);
- }
-}
-
-async function loadGame(gameId) {
- try {
- const [lobbies, players] = await Promise.all([
- fetchJson(`/stats/${gameId}/lobbies`),
- fetchJson(`/stats/${gameId}/players`)
- ]);
- document.getElementById('gameLobbies').textContent = lobbies;
- document.getElementById('gamePlayers').textContent = players.length ?? players; // endpoint returns list
- } catch (e) {
- console.error(e);
- }
-}
-
-async function setGameCookie(gameId) {
- const r = await fetch('/session/game', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ gameId }),
- credentials: 'include'
- });
- if (!r.ok) throw new Error('Failed to set game cookie');
- return r.json();
-}
-
-function getCookie(name) {
- const m = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
- return m ? decodeURIComponent(m[2]) : null;
-}
-
-(async () => {
- await loadGlobal();
-
- const gameForm = document.getElementById('gameForm');
- const gameIdInput = document.getElementById('gameIdInput');
- const cookieStatus = document.getElementById('cookieStatus');
-
- //if cookie exists prefill and load stats
- const existing = getCookie('gameId');
- if (existing) {
- gameIdInput.value = existing;
- loadGame(existing);
- cookieStatus.textContent = `Using gameId from cookie.`;
- }
-
- gameForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- const gameId = gameIdInput.value.trim();
- if (!/^\{?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\}?$/.test(gameId)) {
- alert('Please enter a valid GUID');
- return;
- }
- try {
- await setGameCookie(gameId);
- cookieStatus.textContent = 'GameId stored in cookie.';
- await loadGame(gameId);
- } catch (err) {
- cookieStatus.textContent = 'Failed to set cookie';
- console.error(err);
- }
- });
-})();
diff --git a/PurrLobby/wwwroot/styles.css b/PurrLobby/wwwroot/styles.css
deleted file mode 100644
index 36c9453..0000000
--- a/PurrLobby/wwwroot/styles.css
+++ /dev/null
@@ -1,115 +0,0 @@
-:root {
- --bg: #000;
- --text: #fff;
- --muted: #aaa;
- --border: #333;
- --accent: #2aa9ff;
-}
-
-* {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
-}
-
-body {
- background: var(--bg);
- color: var(--text);
- font-family: monospace, sans-serif;
- line-height: 1.5;
-}
-
-.container {
- max-width: 800px;
- margin: 20px auto;
- padding: 0 12px;
-}
-
-.hero {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-bottom: 20px;
-}
-
-.logo {
- width: 64px;
- height: 64px;
- object-fit: contain;
-}
-
-.title {
- font-size: 32px;
-}
-
-.section-title {
- font-size: 14px;
- margin: 20px 0 8px;
- text-transform: uppercase;
-}
-
-.center {
- text-align: center;
-}
-
-.bigstats {
- display: flex;
- gap: 20px;
- justify-content: center;
- flex-wrap: wrap;
- margin-bottom: 20px;
-}
-
-.bigstat {
- min-width: 120px;
- display: flex;
- flex-direction: column;
- align-items: center;
-}
-
-.bignum {
- font-size: 32px;
- font-weight: bold;
- text-align: center;
-}
-
-.biglabel {
- font-size: 12px;
- color: var(--muted);
- text-align: center;
-}
-
-.stack {
- display: grid;
- gap: 8px;
- margin-bottom: 20px;
-}
-
-#gameIdInput {
- padding: 8px;
- border: 1px solid var(--border);
- background: var(--bg);
- color: var(--text);
-}
-
-button {
- padding: 8px;
- border: 1px solid var(--border);
- background: #111;
- color: var(--text);
- cursor: pointer;
-}
-
- button:hover {
- background: #222;
- }
-
-.disclaimer {
- font-size: 13px;
- color: var(--muted);
- margin-top: 20px;
-}
-
-a {
- color: var(--accent);
-}
diff --git a/README.Docker.md b/README.Docker.md
new file mode 100644
index 0000000..4ec1f02
--- /dev/null
+++ b/README.Docker.md
@@ -0,0 +1,24 @@
+### Building and running your application
+
+When you're ready, start your application by running:
+`docker compose up --build`.
+
+Your application will be available at http://localhost:443.
+
+### Deploying your application to the cloud
+
+First, build your image, e.g.: `docker build -t myapp .`.
+If your cloud uses a different CPU architecture than your development
+machine (e.g., you are on a Mac M1 and your cloud provider is amd64),
+you'll want to build the image for that platform, e.g.:
+`docker build --platform=linux/amd64 -t myapp .`.
+
+Then, push it to your registry, e.g. `docker push myregistry.com/myapp`.
+
+Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/)
+docs for more detail on building and pushing.
+
+### References
+* [Docker's .NET guide](https://docs.docker.com/language/dotnet/)
+* The [dotnet-docker](https://github.com/dotnet/dotnet-docker/tree/main/samples)
+ repository has many relevant samples and docs.
\ No newline at end of file
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..4f0013c
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,51 @@
+# Comments are provided throughout this file to help you get started.
+# If you need more help, visit the Docker Compose reference guide at
+# https://docs.docker.com/go/compose-spec-reference/
+
+# Here the instructions define your application as a service called "server".
+# This service is built from the Dockerfile in the current directory.
+# You can add other services your application may depend on here, such as a
+# database or a cache. For examples, see the Awesome Compose repository:
+# https://github.com/docker/awesome-compose
+services:
+ server:
+ build:
+ context: .
+ target: final
+ ports:
+ - 443:8080
+ network_mode: "host"
+# The commented out section below is an example of how to define a PostgreSQL
+# database that your application can use. `depends_on` tells Docker Compose to
+# start the database before your application. The `db-data` volume persists the
+# database data between container restarts. The `db-password` secret is used
+# to set the database password. You must create `db/password.txt` and add
+# a password of your choosing to it before running `docker compose up`.
+# depends_on:
+# db:
+# condition: service_healthy
+# db:
+# image: postgres
+# restart: always
+# user: postgres
+# secrets:
+# - db-password
+# volumes:
+# - db-data:/var/lib/postgresql/data
+# environment:
+# - POSTGRES_DB=example
+# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
+# expose:
+# - 5432
+# healthcheck:
+# test: [ "CMD", "pg_isready" ]
+# interval: 10s
+# timeout: 5s
+# retries: 5
+
+# volumes:
+# db-data:
+# secrets:
+# db-password:
+# file: db/password.txt
+