@@ -3,8 +3,8 @@ import { AppHandler } from '~/lib/handler'
33import { getConnection } from '~/lib/db/connection'
44import { createRoute } from '@hono/zod-openapi'
55import { GenericResponses } from '~/lib/response-schemas'
6- import { asset , user } from '~/lib/db/schema'
7- import { eq , inArray } from 'drizzle-orm'
6+ import { asset , user , game , category , tag , assetToTag } from '~/lib/db/schema'
7+ import { eq , inArray , desc } from 'drizzle-orm'
88import { requireAuth , requireAdmin } from '~/lib/auth/middleware'
99
1010const responseSchema = z . object ( {
@@ -13,12 +13,35 @@ const responseSchema = z.object({
1313 z . object ( {
1414 id : z . string ( ) ,
1515 name : z . string ( ) ,
16+ gameId : z . string ( ) ,
17+ categoryId : z . string ( ) ,
18+ extension : z . string ( ) ,
1619 status : z . string ( ) ,
1720 uploadedBy : z . object ( {
1821 id : z . string ( ) ,
1922 username : z . string ( ) . nullable ( ) ,
2023 image : z . string ( ) . nullable ( ) ,
2124 } ) ,
25+ game : z . object ( {
26+ id : z . string ( ) ,
27+ slug : z . string ( ) ,
28+ name : z . string ( ) ,
29+ lastUpdated : z . string ( ) ,
30+ assetCount : z . number ( ) ,
31+ } ) ,
32+ category : z . object ( {
33+ id : z . string ( ) ,
34+ name : z . string ( ) ,
35+ slug : z . string ( ) ,
36+ } ) ,
37+ tags : z . array (
38+ z . object ( {
39+ id : z . string ( ) ,
40+ name : z . string ( ) ,
41+ slug : z . string ( ) ,
42+ color : z . string ( ) . nullable ( ) ,
43+ } ) ,
44+ ) ,
2245 } ) ,
2346 ) ,
2447} )
@@ -42,10 +65,25 @@ const approvalQueueRoute = createRoute({
4265 } ,
4366} )
4467
68+ const paramsSchema = z . object ( {
69+ id : z . string ( ) . openapi ( {
70+ param : {
71+ description : 'The asset ID' ,
72+ in : 'path' ,
73+ name : 'id' ,
74+ required : true ,
75+ } ,
76+ example : 'asset_123' ,
77+ } ) ,
78+ } )
79+
4580const approveRoute = createRoute ( {
46- path : '/:id /approve' ,
81+ path : '/{id} /approve' ,
4782 method : 'post' ,
4883 summary : 'Approve asset' ,
84+ request : {
85+ params : paramsSchema ,
86+ } ,
4987 description : 'Approve an asset. Admin only.' ,
5088 tags : [ 'Asset' ] ,
5189 responses : {
@@ -58,9 +96,12 @@ const approveRoute = createRoute({
5896} )
5997
6098const denyRoute = createRoute ( {
61- path : '/:id /deny' ,
99+ path : '/{id} /deny' ,
62100 method : 'post' ,
63101 summary : 'Deny asset' ,
102+ request : {
103+ params : paramsSchema ,
104+ } ,
64105 description : 'Deny an asset. Admin only.' ,
65106 tags : [ 'Asset' ] ,
66107 responses : {
@@ -76,37 +117,104 @@ export const AssetApprovalQueueRoute = (handler: AppHandler) => {
76117 handler . use ( '/approval-queue' , requireAuth , requireAdmin )
77118 handler . openapi ( approvalQueueRoute , async ctx => {
78119 const { drizzle } = getConnection ( ctx . env )
79- const pendingAssets = await drizzle . select ( ) . from ( asset ) . where ( eq ( asset . status , 'pending' ) )
120+ const pendingAssets = await drizzle
121+ . select ( {
122+ id : asset . id ,
123+ name : asset . name ,
124+ downloadCount : asset . downloadCount ,
125+ viewCount : asset . viewCount ,
126+ size : asset . size ,
127+ extension : asset . extension ,
128+ status : asset . status ,
129+ createdAt : asset . createdAt ,
130+ gameId : game . id ,
131+ gameSlug : game . slug ,
132+ gameName : game . name ,
133+ gameLastUpdated : game . lastUpdated ,
134+ gameAssetCount : game . assetCount ,
135+ categoryId : category . id ,
136+ categoryName : category . name ,
137+ categorySlug : category . slug ,
138+ isSuggestive : asset . isSuggestive ,
139+ uploadedBy : asset . uploadedBy ,
140+ } )
141+ . from ( asset )
142+ . innerJoin ( game , eq ( asset . gameId , game . id ) )
143+ . innerJoin ( category , eq ( asset . categoryId , category . id ) )
144+ . innerJoin ( user , eq ( asset . uploadedBy , user . id ) )
145+ . where ( eq ( asset . status , 'pending' ) )
146+ . orderBy ( desc ( asset . createdAt ) )
147+
148+ const assetTags = await drizzle
149+ . select ( {
150+ tagId : tag . id ,
151+ tagName : tag . name ,
152+ tagSlug : tag . slug ,
153+ tagColor : tag . color ,
154+ } )
155+ . from ( assetToTag )
156+ . innerJoin ( tag , eq ( assetToTag . tagId , tag . id ) )
157+ . where (
158+ inArray (
159+ assetToTag . assetId ,
160+ pendingAssets . map ( a => a . id ) ,
161+ ) ,
162+ )
163+
80164 const uploaderIds = pendingAssets . map ( a => a . uploadedBy )
81- const uploaders =
82- uploaderIds . length > 0
83- ? await drizzle
84- . select ( {
85- id : user . id ,
86- username : user . username ,
87- image : user . image ,
88- } )
89- . from ( user )
90- . where ( inArray ( user . id , uploaderIds ) )
91- : [ ]
165+ const uploaders = await drizzle
166+ . select ( {
167+ id : user . id ,
168+ username : user . username ,
169+ image : user . image ,
170+ } )
171+ . from ( user )
172+ . where ( inArray ( user . id , uploaderIds ) )
173+
92174 const uploaderMap = Object . fromEntries ( uploaders . map ( u => [ u . id , u ] ) )
93- const assets = pendingAssets . map ( a => ( {
175+
176+ const formattedAssets = pendingAssets . map ( a => ( {
94177 id : a . id ,
95178 name : a . name ,
96179 status : a . status ,
97- uploadedBy : uploaderMap [ a . uploadedBy ] || { id : a . uploadedBy , username : null , image : null } ,
180+ gameId : a . gameId ,
181+ categoryId : a . categoryId ,
182+ extension : a . extension ,
183+ uploadedBy : uploaderMap [ a . uploadedBy ] ! ,
184+ game : {
185+ id : a . gameId ,
186+ slug : a . gameSlug ,
187+ name : a . gameName ,
188+ lastUpdated : a . gameLastUpdated ,
189+ assetCount : a . gameAssetCount ,
190+ } ,
191+ category : {
192+ id : a . categoryId ,
193+ name : a . categoryName ,
194+ slug : a . categorySlug ,
195+ } ,
196+ tags : assetTags . map ( t => ( {
197+ id : t . tagId ,
198+ name : t . tagName ,
199+ slug : t . tagSlug ,
200+ color : t . tagColor ,
201+ } ) ) ,
98202 } ) )
99- return ctx . json ( { success : true , assets } , 200 )
203+
204+ return ctx . json ( { success : true , assets : formattedAssets } , 200 )
100205 } )
101206}
102207
103208export const AssetApproveRoute = ( handler : AppHandler ) => {
104- handler . use ( '/:id /approve' , requireAuth , requireAdmin )
209+ handler . use ( '/{id} /approve' , requireAuth , requireAdmin )
105210 handler . openapi ( approveRoute , async ctx => {
106211 const user = ctx . get ( 'user' )
107212
108- const id = ctx . req . param ( 'id' )
213+ if ( ! user ) {
214+ return ctx . json ( { success : false , message : 'User context failed' } , 401 )
215+ }
109216
217+ const id = ctx . req . param ( 'id' )
110218 const { drizzle } = getConnection ( ctx . env )
111219
112220 const [ foundAsset ] = await drizzle . select ( ) . from ( asset ) . where ( eq ( asset . id , id ) )
@@ -117,18 +225,29 @@ export const AssetApproveRoute = (handler: AppHandler) => {
117225
118226 await drizzle . update ( asset ) . set ( { status : 'approved' } ) . where ( eq ( asset . id , id ) )
119227
228+ const file = await ctx . env . CDN . get ( `limbo/${ foundAsset . id } .${ foundAsset . extension } ` )
229+
230+ if ( ! file ) {
231+ return ctx . json ( { success : false , message : 'Asset file not found' } , 404 )
232+ }
233+
234+ await ctx . env . CDN . put ( `asset/${ foundAsset . id } .${ foundAsset . extension } ` , file . body )
235+ await ctx . env . CDN . delete ( `limbo/${ foundAsset . id } .${ foundAsset . extension } ` )
236+
120237 if ( ctx . env . DISCORD_WEBHOOK ) {
121238 try {
122239 await fetch ( ctx . env . DISCORD_WEBHOOK , {
123240 method : 'POST' ,
241+ headers : { 'Content-Type' : 'application/json' } ,
124242 body : JSON . stringify ( {
125243 content : null ,
126244 embeds : [
127245 {
128- description : `Approved ${ foundAsset . name } [ ${ foundAsset . extension } ]` ,
246+ description : `Approved [ ${ foundAsset . name } ](https://wanderer.moe/asset/ ${ foundAsset . id } ) [. ${ foundAsset . extension . toUpperCase ( ) } ]` ,
129247 color : 3669788 ,
130248 author : {
131249 name : user . username ,
250+ icon_url : user . image || undefined ,
132251 } ,
133252 footer : {
134253 text : `${ foundAsset . gameId } - ${ foundAsset . categoryId } ` ,
@@ -149,10 +268,14 @@ export const AssetApproveRoute = (handler: AppHandler) => {
149268}
150269
151270export const AssetDenyRoute = ( handler : AppHandler ) => {
152- handler . use ( '/:id /deny' , requireAuth , requireAdmin )
271+ handler . use ( '/{id} /deny' , requireAuth , requireAdmin )
153272 handler . openapi ( denyRoute , async ctx => {
154273 const user = ctx . get ( 'user' )
155274
275+ if ( ! user ) {
276+ return ctx . json ( { success : false , message : 'User context failed' } , 401 )
277+ }
278+
156279 const { drizzle } = getConnection ( ctx . env )
157280 const id = ctx . req . param ( 'id' )
158281
@@ -162,20 +285,24 @@ export const AssetDenyRoute = (handler: AppHandler) => {
162285 return ctx . json ( { success : false , message : 'Asset not found' } , 404 )
163286 }
164287
165- await drizzle . update ( asset ) . set ( { status : 'denied' } ) . where ( eq ( asset . id , id ) )
288+ await drizzle . delete ( asset ) . where ( eq ( asset . id , id ) )
289+
290+ await ctx . env . CDN . delete ( `limbo/${ foundAsset . id } .${ foundAsset . extension } ` )
166291
167292 if ( ctx . env . DISCORD_WEBHOOK ) {
168293 try {
169294 await fetch ( ctx . env . DISCORD_WEBHOOK , {
170295 method : 'POST' ,
296+ headers : { 'Content-Type' : 'application/json' } ,
171297 body : JSON . stringify ( {
172298 content : null ,
173299 embeds : [
174300 {
175- description : `Denied ${ foundAsset . name } [${ foundAsset . extension } ]` ,
301+ description : `Denied ${ foundAsset . name } [. ${ foundAsset . extension . toUpperCase ( ) } ]` ,
176302 color : 16734039 ,
177303 author : {
178304 name : user . username ,
305+ icon_url : user . image || undefined ,
179306 } ,
180307 footer : {
181308 text : `${ foundAsset . gameId } - ${ foundAsset . categoryId } ` ,
0 commit comments