-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathProgram.cs
More file actions
179 lines (158 loc) · 6.89 KB
/
Copy pathProgram.cs
File metadata and controls
179 lines (158 loc) · 6.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using PlatformSampleGameServer.Endpoints;
using PlatformSampleGameServer.Services;
var builder = WebApplication.CreateBuilder(args);
// Don't remap inbound JWT claim names (e.g. "email" -> long XMLSOAP URI).
// We want claims to round-trip with their original short names so endpoints
// can read them by their well-known JWT names.
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
// ----- Configuration -----
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true);
builder.Configuration.AddEnvironmentVariables();
builder.Services.Configure<ServerOptions>(builder.Configuration.GetSection("Server"));
builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection("Jwt"));
builder.Services.Configure<EnjinOptions>(builder.Configuration.GetSection("Enjin"));
// ----- Services -----
builder.Services.AddSingleton<UserStore>();
builder.Services.AddSingleton<AuthService>();
builder.Services.AddSingleton<ServerState>();
builder.Services.AddSingleton<EnjinService>();
builder.Services.AddCors(o =>
o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod())
);
// JSON: camelCase property names + serialize BigInteger-as-string so the
// Unity client's SerializableBigInteger wrapper can parse it. Records with
// PascalCase properties will be emitted as camelCase by default; this is
// what the Unity client expects (its JsonUtility uses the field name verbatim,
// and its fields are camelCase).
builder.Services.ConfigureHttpJsonOptions(o =>
{
o.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
o.SerializerOptions.PropertyNameCaseInsensitive = true;
});
// ----- JWT auth -----
var jwt = builder.Configuration.GetSection("Jwt").Get<JwtOptions>() ?? new JwtOptions();
if (string.IsNullOrWhiteSpace(jwt.Secret))
{
// Emit a generated dev secret if none is configured so the server still boots
// for local development. Production must set Jwt:Secret explicitly.
jwt.Secret = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N");
Console.WriteLine(
"[warn] Jwt:Secret not configured; generated a transient dev secret. "
+ "Set Jwt:Secret in appsettings or env for stable sessions across restarts."
);
// Propagate the generated secret into the bound JwtOptions so AuthService
// (which resolves IOptions<JwtOptions>) signs tokens with the same key that
// the JwtBearer middleware validates against. Without this, AuthService
// would either throw on an empty Secret or sign with a different key.
builder.Services.PostConfigure<JwtOptions>(o =>
{
if (string.IsNullOrWhiteSpace(o.Secret))
o.Secret = jwt.Secret;
});
}
builder
.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts =>
{
opts.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = !string.IsNullOrEmpty(jwt.Issuer),
ValidIssuer = jwt.Issuer,
ValidateAudience = !string.IsNullOrEmpty(jwt.Audience),
ValidAudience = jwt.Audience,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt.Secret)),
ClockSkew = TimeSpan.FromMinutes(1),
};
});
builder.Services.AddAuthorization();
// ----- Kestrel: port + request timeout -----
var serverOptions =
builder.Configuration.GetSection("Server").Get<ServerOptions>() ?? new ServerOptions();
builder.WebHost.ConfigureKestrel(k =>
{
k.ListenAnyIP(serverOptions.Port);
});
// Long-running on-chain operations (mint/melt/transfer) can legitimately take
// several minutes while we poll for finalization. Configure a per-request
// execution timeout via the ASP.NET Core RequestTimeouts middleware so the
// server cancels requests that exceed Server:RequestTimeoutSeconds instead of
// letting them run indefinitely. (KeepAliveTimeout is intentionally left at
// its default; it controls idle connection lifetime, not request duration.)
builder.Services.AddRequestTimeouts(o =>
{
o.DefaultPolicy = new Microsoft.AspNetCore.Http.Timeouts.RequestTimeoutPolicy
{
Timeout = TimeSpan.FromSeconds(serverOptions.RequestTimeoutSeconds),
};
});
var app = builder.Build();
app.UseCors();
app.UseRequestTimeouts();
app.UseAuthentication();
app.UseAuthorization();
// ----- Routes -----
app.MapAuthEndpoints();
app.MapWalletEndpoints();
app.MapTokenEndpoints();
app.MapSetupEndpoints();
// ----- Bootstrap the collection + resource tokens before serving any requests -----
// Pass --skip-bootstrap (or set Enjin:SkipBootstrap=true) to start the server
// without creating/verifying the on-chain collection. Useful for local smoke tests
// where you don't want to mutate canary.
var skipBootstrap =
args.Contains("--skip-bootstrap")
|| string.Equals(
builder.Configuration["Enjin:SkipBootstrap"],
"true",
StringComparison.OrdinalIgnoreCase
);
using (var scope = app.Services.CreateScope())
{
var sp = scope.ServiceProvider;
var state = sp.GetRequiredService<ServerState>();
// Allow the operator to seed state.json on first run by supplying a
// collection id via configuration. Configuration:AddEnvironmentVariables
// accepts the standard double-underscore form (Enjin__CollectionId), but
// the legacy Node.js sample used the flat ENJIN_COLLECTION_ID name, so
// we accept that too as a fallback for operators migrating from the old
// server.
var collectionIdSeed =
builder.Configuration["Enjin:CollectionId"]
?? Environment.GetEnvironmentVariable("ENJIN_COLLECTION_ID");
state.OverrideFromConfig(collectionIdSeed);
var log = sp.GetRequiredService<ILogger<Program>>();
if (skipBootstrap)
{
log.LogWarning(
"Bootstrap skipped (--skip-bootstrap). On-chain operations may fail until a collection ID is provided."
);
}
else
{
var enjin = sp.GetRequiredService<EnjinService>();
log.LogInformation(
"Preparing collection and resource tokens. This may take a few minutes on first run."
);
try
{
await enjin.PrepareCollectionAsync(CancellationToken.None);
}
catch (Exception ex)
{
log.LogCritical(ex, "Failed to prepare collection. Server will not start.");
return;
}
}
log.LogInformation("----------------------------------------");
log.LogInformation("Collection ID: {Id}", state.CollectionId?.ToString() ?? "(unset)");
log.LogInformation("Server listening on http://0.0.0.0:{Port}", serverOptions.Port);
log.LogInformation("----------------------------------------");
}
await app.RunAsync();