11using Microsoft . AspNetCore . Authorization ;
22using Microsoft . EntityFrameworkCore ;
33using System . Security . Claims ;
4+ using System . Security . Cryptography . X509Certificates ;
45using System . Text ;
6+ using System . Text . Json ;
57
68namespace 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: <your key></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: <your key></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