diff --git a/.github/leaderboard.png b/.github/leaderboard.png new file mode 100644 index 0000000..10b9ce2 Binary files /dev/null and b/.github/leaderboard.png differ diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml new file mode 100644 index 0000000..2b09553 --- /dev/null +++ b/.github/workflows/dotnet-build.yml @@ -0,0 +1,79 @@ +name: .NET Build + +on: + push: + branches: + - '**' +env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: install wasm workload (macOS) + if: matrix.os == 'macos-latest' + run: sudo dotnet workload install wasm-tools + + - name: install wasm workload (Windows/Linux) + if: matrix.os != 'macos-latest' + run: dotnet workload install wasm-tools + + - run: dotnet build -c Release + + - name: publish Web app + run: dotnet publish Sentaur.Leaderboard.Web -c Release -o webapp + + - name: upload web app to artifact + uses: actions/upload-artifact@v4 + if: matrix.os == 'ubuntu-latest' + with: + name: webapp + if-no-files-found: error + retention-days: 10 + path: | + ${{ github.workspace }}/webapp/ + + deploy: + needs: build + if: ${{ github.ref == 'refs/heads/main' }} + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + contents: read + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + concurrency: + group: "pages" + cancel-in-progress: false + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + steps: + - name: Download a single artifact + uses: actions/download-artifact@v4 + with: + name: webapp + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'wwwroot' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 5e57f18..d4e88f9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ ## ## Get latest from `dotnet new gitignore` +# output dir for publish blazor app as used on GHA build yml +webapp/ + # dotenv files .env diff --git a/Sentaur.Leaderboard.Api/Dockerfile b/Dockerfile similarity index 86% rename from Sentaur.Leaderboard.Api/Dockerfile rename to Dockerfile index ced88df..e3c6e89 100644 --- a/Sentaur.Leaderboard.Api/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ EXPOSE 8081 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG BUILD_CONFIGURATION=Release +ARG SENTRY_AUTH_TOKEN +ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN WORKDIR /src COPY ["Sentaur.Leaderboard.Api/Sentaur.Leaderboard.Api.csproj", "Sentaur.Leaderboard.Api/"] RUN dotnet restore "Sentaur.Leaderboard.Api/Sentaur.Leaderboard.Api.csproj" @@ -15,6 +17,8 @@ RUN dotnet build "Sentaur.Leaderboard.Api.csproj" -c $BUILD_CONFIGURATION -o /ap FROM build AS publish ARG BUILD_CONFIGURATION=Release +ARG SENTRY_AUTH_TOKEN +ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN RUN dotnet publish "Sentaur.Leaderboard.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false FROM base AS final diff --git a/README.md b/README.md index fa17ace..fa9d915 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ -# game-leaderboard \ No newline at end of file +# Leaderboard for Sentaur Survivors + +[https://sentaur-survivor.com/](https://sentaur-survivor.com/) + +![Leaderboard for Sentaur Survivors](.github/leaderboard.png?raw=true "Leaderboard") diff --git a/Sentaur.Leaderboard.AntiCheat/AntiCheatFunction.cs b/Sentaur.Leaderboard.AntiCheat/AntiCheatFunction.cs index 9ae4395..b07897e 100644 --- a/Sentaur.Leaderboard.AntiCheat/AntiCheatFunction.cs +++ b/Sentaur.Leaderboard.AntiCheat/AntiCheatFunction.cs @@ -1,9 +1,12 @@ using System.Globalization; using System.Text.Json; using Google.Cloud.Functions.Framework; +using Google.Cloud.Functions.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +[assembly: FunctionsStartup(typeof(SentryStartup))] + namespace Sentaur.Leaderboard.AntiCheat; public class Function(ILogger _logger) : IHttpFunction diff --git a/Sentaur.Leaderboard.AntiCheat/Sentaur.Leaderboard.AntiCheat.csproj b/Sentaur.Leaderboard.AntiCheat/Sentaur.Leaderboard.AntiCheat.csproj index a134e94..f24d3df 100644 --- a/Sentaur.Leaderboard.AntiCheat/Sentaur.Leaderboard.AntiCheat.csproj +++ b/Sentaur.Leaderboard.AntiCheat/Sentaur.Leaderboard.AntiCheat.csproj @@ -9,6 +9,21 @@ - + + + + + false + PreserveNewest + PreserveNewest + + + + + demo + sentaur-survivor-anticheat + true + true + diff --git a/Sentaur.Leaderboard.AntiCheat/appsettings.json b/Sentaur.Leaderboard.AntiCheat/appsettings.json new file mode 100644 index 0000000..7caa141 --- /dev/null +++ b/Sentaur.Leaderboard.AntiCheat/appsettings.json @@ -0,0 +1,6 @@ +{ + "Sentry": { + "EnableTracing": true, + "Debug": true + } +} diff --git a/Sentaur.Leaderboard.Api/JwtTokenHolder.cs b/Sentaur.Leaderboard.Api/JwtTokenHolder.cs new file mode 100644 index 0000000..68f3c7d --- /dev/null +++ b/Sentaur.Leaderboard.Api/JwtTokenHolder.cs @@ -0,0 +1,20 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace Sentaur.Leaderboard.Api; + +public class JwtTokenHolder +{ + public string Token { get; } + + public JwtTokenHolder(WebApplicationBuilder builder) + { + var issuer = builder.Configuration["Jwt:Issuer"]; + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("Failed to get 'Jwt:Key'"))); + var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken(issuer: issuer, signingCredentials: credentials); + Token = new JwtSecurityTokenHandler().WriteToken(token); + } +} \ No newline at end of file diff --git a/Sentaur.Leaderboard.Api/LeaderboardContext.cs b/Sentaur.Leaderboard.Api/LeaderboardContext.cs index dedfd6c..4bda7a6 100644 --- a/Sentaur.Leaderboard.Api/LeaderboardContext.cs +++ b/Sentaur.Leaderboard.Api/LeaderboardContext.cs @@ -1,20 +1,12 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Sentaur.Leaderboard.Api; -public class LeaderboardContext : DbContext +public class LeaderboardContext(DbContextOptions options) : DbContext(options) { public DbSet ScoreEntries { get; set; } = null!; - private string _dbPath; - - public LeaderboardContext() - { - var folder = Environment.SpecialFolder.LocalApplicationData; - var path = Environment.GetFolderPath(folder); - _dbPath = Path.Join(path, "scores.db"); - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder @@ -24,5 +16,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } protected override void OnConfiguring(DbContextOptionsBuilder options) - => options.UseSqlite($"Data Source={_dbPath}"); + => options.UseNpgsql(); + + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + configurationBuilder + .Properties() + .HaveConversion(); + } } + +public class DateTimeOffsetConverter() + : ValueConverter(d => d.ToUniversalTime(), + d => d.ToUniversalTime()); diff --git a/Sentaur.Leaderboard.Api/Migrations/20240301161657_InitialCreate.Designer.cs b/Sentaur.Leaderboard.Api/Migrations/20240301161657_InitialCreate.Designer.cs deleted file mode 100644 index 830365c..0000000 --- a/Sentaur.Leaderboard.Api/Migrations/20240301161657_InitialCreate.Designer.cs +++ /dev/null @@ -1,47 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Sentaur.Leaderboard.Api; - -#nullable disable - -namespace Sentaur.Leaderboard.Api.Migrations -{ - [DbContext(typeof(LeaderboardContext))] - [Migration("20240301161657_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.2"); - - modelBuilder.Entity("Sentaur.Leaderboard.ScoreEntry", b => - { - b.Property("Duration") - .HasColumnType("TEXT"); - - b.Property("Email") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Score") - .HasColumnType("INTEGER"); - - b.Property("Timestamp") - .HasColumnType("TEXT"); - - b.ToTable("ScoreEntries"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Sentaur.Leaderboard.Api/Migrations/20240301164227_NewKeyProp.cs b/Sentaur.Leaderboard.Api/Migrations/20240301164227_NewKeyProp.cs deleted file mode 100644 index 69e6845..0000000 --- a/Sentaur.Leaderboard.Api/Migrations/20240301164227_NewKeyProp.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Sentaur.Leaderboard.Api.Migrations -{ - /// - public partial class NewKeyProp : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Key", - table: "ScoreEntries", - type: "TEXT", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); - - migrationBuilder.AddPrimaryKey( - name: "PK_ScoreEntries", - table: "ScoreEntries", - column: "Key"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropPrimaryKey( - name: "PK_ScoreEntries", - table: "ScoreEntries"); - - migrationBuilder.DropColumn( - name: "Key", - table: "ScoreEntries"); - } - } -} diff --git a/Sentaur.Leaderboard.Api/Migrations/20240301164227_NewKeyProp.Designer.cs b/Sentaur.Leaderboard.Api/Migrations/20240314033003_Init.Designer.cs similarity index 64% rename from Sentaur.Leaderboard.Api/Migrations/20240301164227_NewKeyProp.Designer.cs rename to Sentaur.Leaderboard.Api/Migrations/20240314033003_Init.Designer.cs index 4be2b4a..329b562 100644 --- a/Sentaur.Leaderboard.Api/Migrations/20240301164227_NewKeyProp.Designer.cs +++ b/Sentaur.Leaderboard.Api/Migrations/20240314033003_Init.Designer.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Sentaur.Leaderboard.Api; #nullable disable @@ -11,37 +12,41 @@ namespace Sentaur.Leaderboard.Api.Migrations { [DbContext(typeof(LeaderboardContext))] - [Migration("20240301164227_NewKeyProp")] - partial class NewKeyProp + [Migration("20240314033003_Init")] + partial class Init { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.2"); + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("Sentaur.Leaderboard.ScoreEntry", b => { b.Property("Key") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("uuid"); b.Property("Duration") - .HasColumnType("TEXT"); + .HasColumnType("interval"); b.Property("Email") .IsRequired() - .HasColumnType("TEXT"); + .HasColumnType("text"); b.Property("Name") .IsRequired() - .HasColumnType("TEXT"); + .HasColumnType("text"); b.Property("Score") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("Timestamp") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.HasKey("Key"); diff --git a/Sentaur.Leaderboard.Api/Migrations/20240301161657_InitialCreate.cs b/Sentaur.Leaderboard.Api/Migrations/20240314033003_Init.cs similarity index 61% rename from Sentaur.Leaderboard.Api/Migrations/20240301161657_InitialCreate.cs rename to Sentaur.Leaderboard.Api/Migrations/20240314033003_Init.cs index 8c27e5a..c2eba70 100644 --- a/Sentaur.Leaderboard.Api/Migrations/20240301161657_InitialCreate.cs +++ b/Sentaur.Leaderboard.Api/Migrations/20240314033003_Init.cs @@ -6,7 +6,7 @@ namespace Sentaur.Leaderboard.Api.Migrations { /// - public partial class InitialCreate : Migration + public partial class Init : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -15,14 +15,16 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "ScoreEntries", columns: table => new { - Name = table.Column(type: "TEXT", nullable: false), - Email = table.Column(type: "TEXT", nullable: false), - Duration = table.Column(type: "TEXT", nullable: false), - Score = table.Column(type: "INTEGER", nullable: false), - Timestamp = table.Column(type: "TEXT", nullable: false) + Key = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Duration = table.Column(type: "interval", nullable: false), + Score = table.Column(type: "integer", nullable: false), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false) }, constraints: table => { + table.PrimaryKey("PK_ScoreEntries", x => x.Key); }); } diff --git a/Sentaur.Leaderboard.Api/Migrations/LeaderboardContextModelSnapshot.cs b/Sentaur.Leaderboard.Api/Migrations/LeaderboardContextModelSnapshot.cs index b1ffe14..39b7a0a 100644 --- a/Sentaur.Leaderboard.Api/Migrations/LeaderboardContextModelSnapshot.cs +++ b/Sentaur.Leaderboard.Api/Migrations/LeaderboardContextModelSnapshot.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Sentaur.Leaderboard.Api; #nullable disable @@ -15,30 +16,34 @@ partial class LeaderboardContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.2"); + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("Sentaur.Leaderboard.ScoreEntry", b => { b.Property("Key") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("uuid"); b.Property("Duration") - .HasColumnType("TEXT"); + .HasColumnType("interval"); b.Property("Email") .IsRequired() - .HasColumnType("TEXT"); + .HasColumnType("text"); b.Property("Name") .IsRequired() - .HasColumnType("TEXT"); + .HasColumnType("text"); b.Property("Score") - .HasColumnType("INTEGER"); + .HasColumnType("integer"); b.Property("Timestamp") - .HasColumnType("TEXT"); + .HasColumnType("timestamp with time zone"); b.HasKey("Key"); diff --git a/Sentaur.Leaderboard.Api/MockData.cs b/Sentaur.Leaderboard.Api/MockData.cs deleted file mode 100644 index 994b0d3..0000000 --- a/Sentaur.Leaderboard.Api/MockData.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Sentaur.Leaderboard.Api; - -internal class MockData -{ - public static List MockScores { get; } = new(); - static MockData() - { - var summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - var mock = Enumerable.Range(1, 5).Select(index => - new ScoreEntry - ( - Guid.NewGuid(), - summaries[Random.Shared.Next(summaries.Length)], - summaries[Random.Shared.Next(summaries.Length)] + "@santry.com", - TimeSpan.FromMinutes(Random.Shared.Next(1, 7)), - Random.Shared.Next(1, 10000), - DateTimeOffset.Now.Add(TimeSpan.FromDays(index)) - )) - .ToArray(); - - MockScores.AddRange(mock); - } -} diff --git a/Sentaur.Leaderboard.Api/Program.cs b/Sentaur.Leaderboard.Api/Program.cs index 79ad222..96cf6ab 100644 --- a/Sentaur.Leaderboard.Api/Program.cs +++ b/Sentaur.Leaderboard.Api/Program.cs @@ -1,56 +1,98 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; using Sentaur.Leaderboard; using Sentaur.Leaderboard.Api; var builder = WebApplication.CreateBuilder(args); -builder.WebHost.UseSentry(); // DSN on appsettings.json +// Add Sentry +builder.WebHost.UseSentry(); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); - -builder.Services.AddCors(options => -{ - options.AddPolicy("AllowAll", - builder => +builder.Services + .AddEndpointsApiExplorer() + .AddSwaggerGen(o => + { + o.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { - builder - .AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader() - .SetPreflightMaxAge(TimeSpan.FromDays(1)); + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "JSON Web Token based security", }); -}); + o.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); + }) + .AddCors(options => + { + options.AddPolicy("AllowAll", + policyBuilder => + { + policyBuilder + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .SetPreflightMaxAge(TimeSpan.FromDays(1)); + }); + }); -builder.Services.AddDbContextFactory( - options => - options.UseSqlite()); +builder.Services + .AddDbContext( + options => options.UseNpgsql(builder.Configuration.GetConnectionString("Leaderboard"))) + .AddAuthorization() + .AddAuthentication(o => + { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(o => + { + o.TokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + ValidateLifetime = false, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] + ?? throw new InvalidOperationException("Failed to get 'Jwt:Key'"))) + }; + }); var app = builder.Build(); -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - +app.UseSwagger(); +app.UseSwaggerUI(); app.UseCors("AllowAll"); - app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); using (var scope = app.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); context.Database.Migrate(); - if (!context.ScoreEntries.Any()) - { - context.ScoreEntries.AddRange(MockData.MockScores); - context.SaveChanges(); - } } -app.MapGet("/score", (LeaderboardContext context, CancellationToken token) => +app.MapGet("/score", [AllowAnonymous] (LeaderboardContext context, CancellationToken token) => { return context.ScoreEntries .OrderByDescending(p => p.Score) @@ -59,12 +101,101 @@ .WithName("scores") .WithOpenApi(); -app.MapPost("/score", async (ScoreEntry scoreEntry, LeaderboardContext context, CancellationToken token) => +app.MapPost("/score", [Authorize] async (ScoreEntry scoreEntry, LeaderboardContext context, CancellationToken token) => { context.ScoreEntries.Add(scoreEntry); await context.SaveChangesAsync(token); }); +var tokenHolder = new JwtTokenHolder(builder); + +app.MapPost("/token", [AllowAnonymous](User user) => +{ + if (user.Username?.Equals(builder.Configuration["User:Username"]) is true && user.Password?.Equals(builder.Configuration["User:Password"]) is true) + { + return Results.Ok(tokenHolder.Token); + } + + return Results.Unauthorized(); +}); + +app.MapDelete("/score", [Authorize] async (string name, int score, LeaderboardContext context, CancellationToken token) => +{ + var entry = await context.ScoreEntries.FirstOrDefaultAsync(p => p.Name == name && p.Score == score, token); + if (entry is not null) + { + context.ScoreEntries.Remove(entry); + await context.SaveChangesAsync(token); + return Results.Ok(); + } + + return Results.Problem($"Failed to remove provided entry with name '{name}' and score '{score}'"); +}); + +app.MapGet("/lottery", [AllowAnonymous] async (LeaderboardContext context, CancellationToken token) => + { + var allResults = await LotteryEntries(context, token); + var winner = Random.Shared.GetItems(allResults.ToArray(), 1); + return winner; + }) + .WithName("lottery") + .WithOpenApi(); + + +app.MapGet("/lottery/entries", LotteryEntries) + .WithName("lottery-entries") + .WithOpenApi(); + +app.MapGet("/throw", (string? text) => +{ + throw new Exception("Testing exception thrown in endpoint: " + text); +}); + app.Run(); +async Task> LotteryEntries(LeaderboardContext leaderboardContext, CancellationToken cancellationToken) +{ + var scoreEntries = await leaderboardContext.ScoreEntries + // Exclude Sentry employees + .Where(p => !p.Email.EndsWith("@sentry.io")) + .OrderByDescending(s => s.Score) + // Dedupe (1 entry per player) + .GroupBy(s => s.Email) + // Get the entry with highest score of each player + .Select(g => g.OrderByDescending(p => p.Score).First()) + .ToListAsync(cancellationToken); + + scoreEntries = scoreEntries.OrderByDescending(s => s.Score).ToList(); + + var credit = new List(); + for (int i = 0; i < 10; i++) + { + var dupe = () => scoreEntries[i] with + { + Key = Guid.NewGuid() + }; + if (i == 0) // 15 entries + { + credit.AddRange(Enumerable.Range(0, 14).Select(_ => dupe())); + } + if (i == 1) // 10 entries + { + credit.AddRange(Enumerable.Range(0, 9).Select(_ => dupe())); + } + if (i == 2) // 7 entries + { + credit.AddRange(Enumerable.Range(0, 6).Select(_ => dupe())); + } + if (i == 3) // 5 entries + { + credit.AddRange(Enumerable.Range(0, 4).Select(_ => dupe())); + } + if (i is > 3 and < 11) // 2 entries + { + credit.AddRange(Enumerable.Range(0, 1).Select(_ => dupe())); + } + } + scoreEntries.AddRange(credit); + return scoreEntries; +} diff --git a/Sentaur.Leaderboard.Api/Sentaur.Leaderboard.Api.csproj b/Sentaur.Leaderboard.Api/Sentaur.Leaderboard.Api.csproj index 2a274a7..75a2ca8 100644 --- a/Sentaur.Leaderboard.Api/Sentaur.Leaderboard.Api.csproj +++ b/Sentaur.Leaderboard.Api/Sentaur.Leaderboard.Api.csproj @@ -7,13 +7,15 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + + @@ -27,4 +29,15 @@ + + + + + + demo + sentaur-survivor-leaderboard-api + true + true + + diff --git a/Sentaur.Leaderboard.Api/Sentaur.Leaderboard.Api.http b/Sentaur.Leaderboard.Api/Sentaur.Leaderboard.Api.http deleted file mode 100644 index e859da2..0000000 --- a/Sentaur.Leaderboard.Api/Sentaur.Leaderboard.Api.http +++ /dev/null @@ -1,6 +0,0 @@ -@Sentaur.Leaderboard.Api_HostAddress = http://localhost:5203 - -GET {{Sentaur.Leaderboard.Api_HostAddress}}/score/ -Accept: application/json - -### diff --git a/Sentaur.Leaderboard.Api/User.cs b/Sentaur.Leaderboard.Api/User.cs new file mode 100644 index 0000000..a9acff8 --- /dev/null +++ b/Sentaur.Leaderboard.Api/User.cs @@ -0,0 +1,7 @@ +namespace Sentaur.Leaderboard.Api; + +public class User +{ + public string? Username { get; set; } + public string? Password { get; set; } +} diff --git a/Sentaur.Leaderboard.Api/appsettings.Development.json b/Sentaur.Leaderboard.Api/appsettings.Development.json index ff66ba6..9110ea0 100644 --- a/Sentaur.Leaderboard.Api/appsettings.Development.json +++ b/Sentaur.Leaderboard.Api/appsettings.Development.json @@ -4,5 +4,15 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "User": { + "Username": "user1", + "Password": "password1" + }, + "Jwt": { + "Key": "This is super secret key for https://sentaur-survivor.com/" + }, + "ConnectionStrings": { + "Leaderboard": "Host=localhost;Database=leaderboard;Username=postgres;Password=leaderboard;Include Error Detail=true;" } } diff --git a/Sentaur.Leaderboard.Api/appsettings.json b/Sentaur.Leaderboard.Api/appsettings.json index f6b21b1..1169ace 100644 --- a/Sentaur.Leaderboard.Api/appsettings.json +++ b/Sentaur.Leaderboard.Api/appsettings.json @@ -7,7 +7,10 @@ }, "AllowedHosts": "*", "Sentry": { - "Dsn": "https://6548a82eaf90c07699d9d04a72c88db5@o447951.ingest.sentry.io/4506836878163969", + "Dsn": "https://5d0a57235954f67390e247d52b9190ca@o87286.ingest.us.sentry.io/4506888440053760", "EnableTracing": true + }, + "Jwt": { + "Issuer": "https://sentaur-survivor.com/" } } diff --git a/Sentaur.Leaderboard.Web/Pages/Score.razor b/Sentaur.Leaderboard.Web/Pages/Score.razor index 4ee9572..f270e74 100644 --- a/Sentaur.Leaderboard.Web/Pages/Score.razor +++ b/Sentaur.Leaderboard.Web/Pages/Score.razor @@ -35,7 +35,7 @@ else @score.Name @score.Score @score.Duration.ToString(@"mm\:ss") - @score.Timestamp.ToString("dddd, h:mm tt") + @score.Timestamp.ToLocalTime().ToString("dddd, h:mm tt") } @@ -48,13 +48,16 @@ else protected override async Task OnInitializedAsync() { // TODO: URL needs to come from config - scores = await Http.GetFromJsonAsync("http://localhost:5203/score"); + const string api = + "https://sentaur-leaderboard-f7z2cjcdzq-uc.a.run.app/score"; + // "http://localhost:5203/score"; + scores = await Http.GetFromJsonAsync(api); } ISpan? _pageLoad; protected override void OnInitialized() { - SentrySdk.ConfigureScope(s => _pageLoad = s.Transaction = SentrySdk.StartTransaction("Score", "page.load")); + SentrySdk.ConfigureScope(s => _pageLoad = s.Transaction = SentrySdk.StartTransaction("leaderboard", "pageload")); base.OnInitialized(); } diff --git a/Sentaur.Leaderboard.Web/Program.cs b/Sentaur.Leaderboard.Web/Program.cs index 338684b..50f36de 100644 --- a/Sentaur.Leaderboard.Web/Program.cs +++ b/Sentaur.Leaderboard.Web/Program.cs @@ -10,7 +10,7 @@ // Blazor built-in integration is in a Draft: https://github.com/getsentry/sentry-dotnet/pull/2569/ builder.Logging.AddSentry(o => { - o.Dsn = "https://780e5db76832242e9c7d7b1a49375cb2@o447951.ingest.sentry.io/4506836940619776"; + o.Dsn = "https://8f3ccd6a8a8e5ba417de8df962236a7d@o87286.ingest.us.sentry.io/4506888120107008"; o.EnableTracing = true; // System.PlatformNotSupportedException: System.Diagnostics.Process is not supported on this platform. diff --git a/Sentaur.Leaderboard.Web/Sentaur.Leaderboard.Web.csproj b/Sentaur.Leaderboard.Web/Sentaur.Leaderboard.Web.csproj index 368461e..e2d1794 100644 --- a/Sentaur.Leaderboard.Web/Sentaur.Leaderboard.Web.csproj +++ b/Sentaur.Leaderboard.Web/Sentaur.Leaderboard.Web.csproj @@ -10,7 +10,7 @@ - + @@ -21,4 +21,11 @@ <_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" /> + + demo + sentaur-survivor-leaderboard-app + true + true + + diff --git a/Sentaur.Leaderboard.Web/wwwroot/favicon.ico b/Sentaur.Leaderboard.Web/wwwroot/favicon.ico new file mode 100644 index 0000000..65d7ccb Binary files /dev/null and b/Sentaur.Leaderboard.Web/wwwroot/favicon.ico differ diff --git a/Sentaur.Leaderboard.Web/wwwroot/favicon.png b/Sentaur.Leaderboard.Web/wwwroot/favicon.png deleted file mode 100644 index 8422b59..0000000 Binary files a/Sentaur.Leaderboard.Web/wwwroot/favicon.png and /dev/null differ diff --git a/Sentaur.Leaderboard.Web/wwwroot/icon-192.png b/Sentaur.Leaderboard.Web/wwwroot/icon-192.png deleted file mode 100644 index 166f56d..0000000 Binary files a/Sentaur.Leaderboard.Web/wwwroot/icon-192.png and /dev/null differ diff --git a/Sentaur.Leaderboard.Web/wwwroot/index.html b/Sentaur.Leaderboard.Web/wwwroot/index.html index 8533bc4..d094d9d 100644 --- a/Sentaur.Leaderboard.Web/wwwroot/index.html +++ b/Sentaur.Leaderboard.Web/wwwroot/index.html @@ -7,7 +7,7 @@ - + @@ -32,6 +32,24 @@ Reload 🗙 + + diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..67cebd2 --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,19 @@ +steps: + - name: 'gcr.io/cloud-builders/docker' + args: [ + 'build', + '-t', 'us.gcr.io/$PROJECT_ID/sentaur-leaderboard-api:latest', + '-t', 'us.gcr.io/$PROJECT_ID/sentaur-leaderboard-api:$COMMIT_SHA', + '--cache-from', 'us.gcr.io/$PROJECT_ID/sentaur-leaderboard-api:latest', + '--build-arg', 'SENTRY_AUTH_TOKEN=$_SENTAUR_SURVIVOR_SENTRY_AUTH_TOKEN', + '--target', 'final', + '.' + ] + + # Only tag "latest" when on main + - name: 'gcr.io/cloud-builders/docker' + entrypoint: 'bash' + args: [ + '-c', + '[[ "$BRANCH_NAME" == "main" ]] && docker push us.gcr.io/$PROJECT_ID/sentaur-leaderboard-api:latest || true', + ]