Skip to content

Commit aa57816

Browse files
authored
Alter db - Implement mod details request
Alter db - Implement mod details request
2 parents 8962750 + 0bbb3d1 commit aa57816

9 files changed

Lines changed: 215 additions & 143 deletions

File tree

.github/workflows/release-zip.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ jobs:
122122
if: steps.api_check.outputs.changed == 'true'
123123
run: |
124124
dotnet restore StarMap.Index.API/StarMap.Index.API.csproj
125-
dotnet pack StarMap.Index.API/StarMap.Index.API.csproj -c Release -o ./nupkg /p:PackageVersion=${{ steps.version.outputs.new }}
125+
dotnet pack StarMap.Index.API/StarMap.Index.API.csproj \
126+
-c Release \
127+
-o ./nupkg \
128+
/p:PackageVersion=${{ steps.version.outputs.new }} \
129+
/p:Version=${{ steps.version.outputs.new }} \
130+
/p:AssemblyVersion=${{ steps.version.outputs.new }} \
131+
/p:FileVersion=${{ steps.version.outputs.new }}
126132
dotnet nuget add source --username "${{ github.actor }}" --password "${{ secrets.GITHUB_TOKEN }}" --store-password-in-clear-text --name github "${{ env.NUGET_SOURCE }}"
127133
dotnet nuget push ./nupkg/*.nupkg --source github --api-key "${{ secrets.GITHUB_TOKEN }}" --skip-duplicate

StarMap.Index.API/ModRepository.proto

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ package StarMap.Index.API;
55
service ModRepositoryService {
66
rpc GetMods (GetModsRequest) returns (GetModsResponse);
77
rpc GetModDetails (GetModDetailsRequest) returns (GetModDetailsResponse);
8-
rpc GetModDownloadLocation (GetModDownloadLocationRequest) returns (GetModDownloadLocationResponse);
98
}
109

11-
message GetModsRequest {}
10+
message GetModsRequest
11+
{
12+
string search = 1;
13+
}
1214

1315
message GetModsResponse {
1416
repeated Mod mods = 1;
@@ -25,7 +27,8 @@ message GetModDetailsResponse
2527

2628
message GetModDownloadLocationRequest
2729
{
28-
Mod mod = 1;
30+
string mod_id = 1;
31+
string version_id = 2;
2932
}
3033

3134
message GetModDownloadLocationResponse
@@ -36,13 +39,19 @@ message GetModDownloadLocationResponse
3639
message Mod {
3740
string id = 1;
3841
string name = 2;
39-
string version = 3;
4042
string author = 4;
4143
}
4244

4345
message ModDetails
4446
{
4547
Mod mod = 1;
46-
repeated string versions = 2;
48+
repeated ModVersion versions = 2;
4749
string description = 3;
50+
}
51+
52+
message ModVersion
53+
{
54+
string id = 1;
55+
string version = 2;
56+
string download_location = 3;
4857
}

StarMap.Index.API/ModRepositoryClient.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,13 @@ public interface IModRespositoryClient : IDisposable
1515
{
1616
Task<Mod[]> GetMods();
1717
Task<ModDetails?> GetModDetails(Guid id);
18-
Task<string> GetModDownloadLocation(Mod mod);
1918
}
2019

2120
public class ModRepositoryClient : IModRespositoryClient
2221
{
2322
private GrpcChannel _channel;
2423
private ModRepositoryService.ModRepositoryServiceClient _client;
25-
//Force build
24+
2625
public ModRepositoryClient(string repositoryUrl)
2726
{
2827
_channel = GrpcChannel.ForAddress(repositoryUrl);
@@ -46,11 +45,6 @@ public async Task<Mod[]> GetMods()
4645
return response.Mod;
4746
}
4847

49-
public Task<string> GetModDownloadLocation(Mod mod)
50-
{
51-
return Task.FromResult("");
52-
}
53-
5448
public void Dispose()
5549
{
5650
_channel.Dispose();

StarMap.Index/DB.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@ public AppDbContext(DbContextOptions<AppDbContext> opts) : base(opts) { }
1414
protected override void OnModelCreating(ModelBuilder mb)
1515
{
1616
mb.Entity<User>().HasIndex(u => u.GithubId).IsUnique();
17-
mb.Entity<User>().HasIndex(u => u.Id).IsUnique();
1817
mb.Entity<Mod>().HasKey(u => u.Id);
1918

20-
mb.Entity<Mod>().HasIndex(m => m.Id).IsUnique();
2119
mb.Entity<Mod>().HasKey(m => m.Id);
2220

2321
mb.Entity<Mod>()
@@ -28,9 +26,16 @@ protected override void OnModelCreating(ModelBuilder mb)
2826
mb.Entity<Mod>()
2927
.HasOne(m => m.LatestVersion)
3028
.WithMany()
31-
.HasForeignKey(m => m.LatestVersionId);
29+
.HasForeignKey(m => m.LatestVersionId)
30+
.OnDelete(DeleteBehavior.Restrict);
3231

3332
mb.Entity<ModVersion>().HasKey(v => v.Id);
33+
mb.Entity<ModVersion>().HasIndex(v => v.ModId);
34+
mb.Entity<ModVersion>()
35+
.HasOne(v => v.Mod)
36+
.WithMany()
37+
.HasForeignKey(v => v.ModId)
38+
.OnDelete(DeleteBehavior.Cascade);
3439
}
3540
}
3641

@@ -57,8 +62,7 @@ public class Mod
5762
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
5863

5964
public Guid? LatestVersionId { get; set; }
60-
public ModVersion? LatestVersion { get; set; } = null!;
61-
public List<ModVersion> Versions { get; set; } = [];
65+
public ModVersion? LatestVersion { get; set; }
6266
}
6367

6468
public class ModVersion
@@ -67,9 +71,18 @@ public class ModVersion
6771
public string Version { get; set; } = "";
6872
public string DownloadUrl { get; set; } = "";
6973
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
74+
75+
public Guid ModId { get; set; }
76+
public Mod Mod { get; set; } = null!;
77+
}
78+
79+
public class AddVersionRequestBody
80+
{
81+
public string Version { get; set; } = "";
82+
public string DownloadUrl { get; set; } = "";
7083
}
7184

72-
public static class CryptoHelpers
85+
public static class CryptoHelpers
7386
{
7487
public static string GenerateApiKey(int bytes = 32)
7588
{

StarMap.Index/Endpoints/ModEndpoints.cs

Lines changed: 140 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using Microsoft.AspNetCore.Authorization;
22
using Microsoft.EntityFrameworkCore;
33
using System.Security.Claims;
4+
using System.Security.Cryptography.X509Certificates;
45
using System.Text;
6+
using System.Text.Json;
57

68
namespace StarMapIndex.Endpoints
79
{
@@ -72,53 +74,99 @@ public static void MapModEndpoints(this IEndpointRouteBuilder app)
7274

7375
// Show the API key once
7476
var html = $@"
75-
<!doctype html><html><body style='font-family:Segoe UI,Arial;padding:16px;'>
76-
<a href='/mods'>← Back</a>
77-
<h2>Mod created: {System.Net.WebUtility.HtmlEncode(name)}</h2>
78-
<p><strong>Save this API key now — you'll only see it once:</strong></p>
79-
<pre style='background:#f3f3f3;padding:8px;border-radius:4px'>{System.Net.WebUtility.HtmlEncode(apiKeyPlain)}</pre>
80-
<p>API Key ID (useful in logs): <code>{System.Net.WebUtility.HtmlEncode(apiKeyId)}</code></p>
81-
<p>To publish: POST to <code>/api/publish</code> with header <code>X-Api-Key: &lt;your key&gt;</code> and JSON body containing <code>name</code>, <code>version</code>, <code>downloadUrl</code>.</p>
82-
</body></html>";
77+
<!doctype html><html><body style='font-family:Segoe UI,Arial;padding:16px;'>
78+
<a href='/mods'>← Back</a>
79+
<h2>Mod created: {System.Net.WebUtility.HtmlEncode(name)}</h2>
80+
<p> Mod id: {System.Net.WebUtility.HtmlEncode(mod.Id.ToString())}</code></p>
81+
<p><strong>Save this API key now — you'll only see it once:</strong></p>
82+
<pre style='background:#f3f3f3;padding:8px;border-radius:4px'>{System.Net.WebUtility.HtmlEncode(apiKeyPlain)}</pre>
83+
<p>API Key ID (useful in logs): <code>{System.Net.WebUtility.HtmlEncode(apiKeyId)}</code></p>
84+
<p>To publish: POST to <code>/api/publish</code> with header <code>X-Api-Key: &lt;your key&gt;</code> and JSON body containing <code>name</code>, <code>version</code>, <code>downloadUrl</code>.</p>
85+
</body></html>";
8386
http.Response.ContentType = "text/html; charset=utf-8";
8487
await http.Response.WriteAsync(html);
8588
});
8689

87-
// Show single mod manage page (owner only)
88-
app.MapGet("/mods/{id:int}", [Authorize] async (int id, HttpContext http, AppDbContext db) =>
90+
app.MapGet("/mods/{id:minlength(36)}", [Authorize] async (string id, HttpContext http, AppDbContext db) =>
8991
{
9092
var userId = Guid.Parse(http.User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
91-
var mod = await db.Mods.FindAsync(id);
92-
if (mod == null || mod.AuthorId != userId)
93+
var mod = await db.Mods.FindAsync(Guid.Parse(id));
94+
if (mod == null)
9395
{
9496
http.Response.StatusCode = 404;
9597
await http.Response.WriteAsync("Not found or not permitted");
9698
return;
9799
}
98100

101+
var versions = await db.Versions
102+
.Where(v => v.ModId == mod.Id)
103+
.OrderByDescending(v => v.CreatedAt)
104+
.ToListAsync();
105+
99106
var html = $@"
100-
<!doctype html><html><body style='font-family:Segoe UI,Arial;padding:16px;'>
101-
<a href='/mods'>← Back</a>
102-
<h2>{System.Net.WebUtility.HtmlEncode(mod.Name)}</h2>
103-
<p>Description: {System.Net.WebUtility.HtmlEncode(mod.Description)}</p>
104-
<p>API Key ID: <code>{System.Net.WebUtility.HtmlEncode(mod.ApiKeyId)}</code></p>
105-
<h3>Versions</h3>
106-
<ul>{string.Join("", (await db.Versions.Where(v => v.Id == mod.Id).OrderByDescending(v => v.Id).ToListAsync()).Select(v => $"<li>{System.Net.WebUtility.HtmlEncode(v.Version)} — <a href=\"{System.Net.WebUtility.HtmlEncode(v.DownloadUrl)}\">download</a> — {v.CreatedAt:O}</li>"))}</ul>
107-
<hr/>
108-
<h3>Rotate API key</h3>
109-
<form method='post' action='/mods/{mod.Id}/rotate'>
110-
<button type='submit'>Rotate (create new API key)</button>
111-
</form>
112-
</body></html>";
107+
<!doctype html><html><body style='font-family:Segoe UI,Arial;padding:16px;'>
108+
<a href='/mods'>← Back</a>
109+
<h2>{System.Net.WebUtility.HtmlEncode(mod.Name)}</h2>
110+
<p>Description: {System.Net.WebUtility.HtmlEncode(mod.Description)}</p>
111+
<p>API Key ID: <code>{System.Net.WebUtility.HtmlEncode(mod.ApiKeyId)}</code></p>
112+
<h3>Versions</h3>
113+
<ul>{string.Join("", (versions).Select(v => $"<li>{System.Net.WebUtility.HtmlEncode(v.Version)} — <a href=\"{System.Net.WebUtility.HtmlEncode(v.DownloadUrl)}\">download</a> — {v.CreatedAt:O}</li>"))}</ul>
114+
<hr/>
115+
<h3>Rotate API key</h3>
116+
<form method='post' action='/mods/{mod.Id}/rotate'>
117+
<button type='submit'>Rotate (create new API key)</button>
118+
</form>
119+
<hr/>
120+
<h3>Danger zone!</h3>
121+
<button id=""delete-btn"" data-id=""{mod.Id}"">Delete mod</button>
122+
</body>
123+
<script>
124+
document.getElementById(""delete-btn"").addEventListener(""click"", async (e) => {{
125+
const id = e.target.dataset.id;
126+
127+
const response = await fetch(`/mods/{id}`, {{
128+
method: ""DELETE"",
129+
headers: {{
130+
""Content-Type"": ""application/json""
131+
}}
132+
}});
133+
window.location.href = ""/mods""
134+
}});
135+
</script>
136+
</html>";
113137
http.Response.ContentType = "text/html; charset=utf-8";
114138
await http.Response.WriteAsync(html);
115139
});
116140

117-
// rotate API key (show new once)
118-
app.MapPost("/mods/{id:int}/rotate", [Authorize] async (int id, HttpContext http, AppDbContext db) =>
141+
app.MapDelete("/mods/{id:minlength(36)}", [Authorize] async (string id, HttpContext http, AppDbContext db) =>
119142
{
120143
var userId = Guid.Parse(http.User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
121-
var mod = await db.Mods.FindAsync(id);
144+
var mod = await db.Mods.FindAsync(Guid.Parse(id));
145+
146+
if (mod == null)
147+
{
148+
http.Response.StatusCode = 404;
149+
await http.Response.WriteAsync("Not found");
150+
return;
151+
}
152+
153+
if (mod.AuthorId != userId)
154+
{
155+
http.Response.StatusCode = 403;
156+
await http.Response.WriteAsync("Forbidden");
157+
return;
158+
}
159+
160+
db.Mods.Remove(mod);
161+
await db.SaveChangesAsync();
162+
163+
http.Response.StatusCode = 200;
164+
});
165+
166+
app.MapPost("/mods/{id:minlength(36)}/rotate", [Authorize] async (string id, HttpContext http, AppDbContext db) =>
167+
{
168+
var userId = Guid.Parse(http.User.FindFirst(ClaimTypes.NameIdentifier)!.Value);
169+
var mod = await db.Mods.FindAsync(Guid.Parse(id));
122170
if (mod == null || mod.AuthorId != userId)
123171
{
124172
http.Response.StatusCode = 404;
@@ -132,15 +180,73 @@ public static void MapModEndpoints(this IEndpointRouteBuilder app)
132180
await db.SaveChangesAsync();
133181

134182
var html = $@"
135-
<!doctype html><html><body style='font-family:Segoe UI,Arial;padding:16px;'>
136-
<a href='/mods/{mod.Id}'>← Back</a>
137-
<h2>New API key for {System.Net.WebUtility.HtmlEncode(mod.Name)}</h2>
138-
<p>Save this key now — it won't be shown again:</p>
139-
<pre style='background:#f3f3f3;padding:8px;border-radius:4px'>{System.Net.WebUtility.HtmlEncode(apiKeyPlain)}</pre>
140-
</body></html>";
183+
<!doctype html><html><body style='font-family:Segoe UI,Arial;padding:16px;'>
184+
<a href='/mods/{mod.Id}'>← Back</a>
185+
<h2>New API key for {System.Net.WebUtility.HtmlEncode(mod.Name)}</h2>
186+
<p>Save this key now — it won't be shown again:</p>
187+
<pre style='background:#f3f3f3;padding:8px;border-radius:4px'>{System.Net.WebUtility.HtmlEncode(apiKeyPlain)}</pre>
188+
</body></html>";
141189
http.Response.ContentType = "text/html; charset=utf-8";
142190
await http.Response.WriteAsync(html);
143191
});
192+
193+
app.MapPost("/mods/{id:minlength(36)}/version", async (string id, HttpContext http, AppDbContext db) =>
194+
{
195+
var mod = await db.Mods.FindAsync(Guid.Parse(id));
196+
197+
if (mod == null)
198+
{
199+
http.Response.StatusCode = 404;
200+
await http.Response.WriteAsync("Not found");
201+
return;
202+
}
203+
204+
if (!http.Request.Headers.TryGetValue("X-Api-Key", out var providedApiKey) || providedApiKey.FirstOrDefault() is not string apiKey)
205+
{
206+
http.Response.StatusCode = 401;
207+
await http.Response.WriteAsync("Missing or malformed API key");
208+
return;
209+
}
210+
var providedApiKeyHash = CryptoHelpers.HashString(apiKey);
211+
212+
if (providedApiKeyHash != mod.ApiKeyHash)
213+
{
214+
http.Response.StatusCode = 403;
215+
await http.Response.WriteAsync("Invalid API key");
216+
return;
217+
}
218+
219+
var newVersion = JsonSerializer.Deserialize<AddVersionRequestBody>(await new StreamReader(http.Request.Body).ReadToEndAsync());
220+
221+
if (newVersion == null || string.IsNullOrEmpty(newVersion.Version) || string.IsNullOrEmpty(newVersion.DownloadUrl))
222+
{
223+
http.Response.StatusCode = 400;
224+
await http.Response.WriteAsync("Invalid request body");
225+
return;
226+
}
227+
228+
await db.Entry(mod).Reference(m => m.LatestVersion).LoadAsync();
229+
230+
if (!Version.TryParse(newVersion.Version, out var parsedNewVersion) || (mod.LatestVersion is not null && Version.TryParse(mod.LatestVersion.Version, out var lastVersion) && parsedNewVersion <= lastVersion))
231+
{
232+
http.Response.StatusCode = 400;
233+
await http.Response.WriteAsync("New version cannot be a lower or equal version than last version");
234+
return;
235+
}
236+
237+
var version = new ModVersion
238+
{
239+
Mod = mod,
240+
Version = newVersion.Version,
241+
DownloadUrl = newVersion.DownloadUrl
242+
};
243+
db.Versions.Add(version);
244+
mod.LatestVersion = version;
245+
await db.SaveChangesAsync();
246+
247+
http.Response.StatusCode = 200;
248+
await http.Response.WriteAsync("Mod version updated successfully");
249+
});
144250
}
145251
}
146252
}

0 commit comments

Comments
 (0)