@@ -148,6 +148,14 @@ fn valid_version(value: &str) -> bool {
148148 . all ( |b| b. is_ascii_alphanumeric ( ) || matches ! ( b, b'.' | b'-' | b'+' ) )
149149}
150150
151+ fn valid_notification_id ( value : & str ) -> bool {
152+ value. starts_with ( "audit_" )
153+ && value. len ( ) <= 160
154+ && value
155+ . bytes ( )
156+ . all ( |byte| byte. is_ascii_alphanumeric ( ) || matches ! ( byte, b'-' | b'_' ) )
157+ }
158+
151159fn github_repository ( value : & str ) -> Option < ( & str , & str ) > {
152160 let ( owner, repository) = value. split_once ( '/' ) ?;
153161 let valid = |part : & str , max : usize | {
@@ -828,6 +836,181 @@ async fn list_developer_releases(req: Request, ctx: RouteContext<()>) -> Result<
828836 json_response ( & json ! ( { "bundle_id" : bundle_id, "releases" : rows} ) , 200 )
829837}
830838
839+ async fn developer_notifications ( req : Request , ctx : RouteContext < ( ) > ) -> Result < Response > {
840+ let actor = match require_developer_actor ( & req, & ctx. env ) . await ? {
841+ Ok ( value) => value,
842+ Err ( response) => return Ok ( response) ,
843+ } ;
844+ let ( limit, offset) = page ( & req) ;
845+ let database = db ( & ctx) ?;
846+ let notifications = store:: developer_notifications (
847+ & database,
848+ & actor. developer_id ,
849+ & actor. account_id ,
850+ limit,
851+ offset,
852+ )
853+ . await ?;
854+ let unread_count =
855+ store:: developer_unread_count ( & database, & actor. developer_id , & actor. account_id ) . await ?;
856+ json_response (
857+ & json ! ( {
858+ "developer_id" : actor. developer_id,
859+ "notifications" : notifications,
860+ "unread_count" : unread_count
861+ } ) ,
862+ 200 ,
863+ )
864+ }
865+
866+ async fn read_developer_notification ( req : Request , ctx : RouteContext < ( ) > ) -> Result < Response > {
867+ if let Some ( response) =
868+ rate_limited ( & req, & ctx. env , "MUTATION_RATE_LIMITER" , "notification-read" ) . await ?
869+ {
870+ return Ok ( response) ;
871+ }
872+ let actor = match require_developer_actor ( & req, & ctx. env ) . await ? {
873+ Ok ( value) => value,
874+ Err ( response) => return Ok ( response) ,
875+ } ;
876+ let notification_id = param ( & ctx, "notification_id" ) ;
877+ if !valid_notification_id ( notification_id) {
878+ return error ( "NOTIFICATION_ID_INVALID" , "Notification ID is invalid" , 422 ) ;
879+ }
880+ if !store:: mark_developer_notification_read (
881+ & db ( & ctx) ?,
882+ notification_id,
883+ & actor. developer_id ,
884+ & actor. account_id ,
885+ now ( ) ,
886+ )
887+ . await ?
888+ {
889+ return error ( "NOTIFICATION_NOT_FOUND" , "Notification not found" , 404 ) ;
890+ }
891+ Ok ( Response :: empty ( ) ?. with_status ( 204 ) )
892+ }
893+
894+ async fn read_all_developer_notifications ( req : Request , ctx : RouteContext < ( ) > ) -> Result < Response > {
895+ if let Some ( response) = rate_limited (
896+ & req,
897+ & ctx. env ,
898+ "MUTATION_RATE_LIMITER" ,
899+ "notifications-read-all" ,
900+ )
901+ . await ?
902+ {
903+ return Ok ( response) ;
904+ }
905+ let actor = match require_developer_actor ( & req, & ctx. env ) . await ? {
906+ Ok ( value) => value,
907+ Err ( response) => return Ok ( response) ,
908+ } ;
909+ store:: mark_all_developer_notifications_read (
910+ & db ( & ctx) ?,
911+ & actor. developer_id ,
912+ & actor. account_id ,
913+ now ( ) ,
914+ )
915+ . await ?;
916+ Ok ( Response :: empty ( ) ?. with_status ( 204 ) )
917+ }
918+
919+ async fn developer_app_history ( req : Request , ctx : RouteContext < ( ) > ) -> Result < Response > {
920+ let actor = match require_developer_actor ( & req, & ctx. env ) . await ? {
921+ Ok ( value) => value,
922+ Err ( response) => return Ok ( response) ,
923+ } ;
924+ let bundle_id = param ( & ctx, "bundle_id" ) ;
925+ let database = db ( & ctx) ?;
926+ if store:: developer_app ( & database, & actor. developer_id , bundle_id)
927+ . await ?
928+ . is_none ( )
929+ {
930+ return error ( "APP_NOT_FOUND" , "App not found" , 404 ) ;
931+ }
932+ json_response (
933+ & json ! ( { "bundle_id" : bundle_id, "history" : store:: app_history( & database, bundle_id) . await ?} ) ,
934+ 200 ,
935+ )
936+ }
937+
938+ async fn admin_notifications ( req : Request , ctx : RouteContext < ( ) > ) -> Result < Response > {
939+ let actor = match require_admin ( & req, & ctx. env ) ? {
940+ Ok ( value) => value,
941+ Err ( response) => return Ok ( response) ,
942+ } ;
943+ let ( limit, offset) = page ( & req) ;
944+ let database = db ( & ctx) ?;
945+ json_response (
946+ & json ! ( {
947+ "notifications" : store:: operator_notifications( & database, & actor, limit, offset) . await ?,
948+ "unread_count" : store:: operator_unread_count( & database, & actor) . await ?
949+ } ) ,
950+ 200 ,
951+ )
952+ }
953+
954+ async fn read_admin_notification ( req : Request , ctx : RouteContext < ( ) > ) -> Result < Response > {
955+ if let Some ( response) = rate_limited (
956+ & req,
957+ & ctx. env ,
958+ "MUTATION_RATE_LIMITER" ,
959+ "admin-notification-read" ,
960+ )
961+ . await ?
962+ {
963+ return Ok ( response) ;
964+ }
965+ let actor = match require_admin ( & req, & ctx. env ) ? {
966+ Ok ( value) => value,
967+ Err ( response) => return Ok ( response) ,
968+ } ;
969+ let notification_id = param ( & ctx, "notification_id" ) ;
970+ if !valid_notification_id ( notification_id) {
971+ return error ( "NOTIFICATION_ID_INVALID" , "Notification ID is invalid" , 422 ) ;
972+ }
973+ if !store:: mark_operator_notification_read ( & db ( & ctx) ?, notification_id, & actor, now ( ) ) . await ? {
974+ return error ( "NOTIFICATION_NOT_FOUND" , "Notification not found" , 404 ) ;
975+ }
976+ Ok ( Response :: empty ( ) ?. with_status ( 204 ) )
977+ }
978+
979+ async fn read_all_admin_notifications ( req : Request , ctx : RouteContext < ( ) > ) -> Result < Response > {
980+ if let Some ( response) = rate_limited (
981+ & req,
982+ & ctx. env ,
983+ "MUTATION_RATE_LIMITER" ,
984+ "admin-notifications-read-all" ,
985+ )
986+ . await ?
987+ {
988+ return Ok ( response) ;
989+ }
990+ let actor = match require_admin ( & req, & ctx. env ) ? {
991+ Ok ( value) => value,
992+ Err ( response) => return Ok ( response) ,
993+ } ;
994+ store:: mark_all_operator_notifications_read ( & db ( & ctx) ?, & actor, now ( ) ) . await ?;
995+ Ok ( Response :: empty ( ) ?. with_status ( 204 ) )
996+ }
997+
998+ async fn admin_release_history ( req : Request , ctx : RouteContext < ( ) > ) -> Result < Response > {
999+ let actor = match require_admin ( & req, & ctx. env ) ? {
1000+ Ok ( value) => value,
1001+ Err ( response) => return Ok ( response) ,
1002+ } ;
1003+ let release_id = param ( & ctx, "release_id" ) ;
1004+ let database = db ( & ctx) ?;
1005+ if store:: release_by_id ( & database, release_id) . await ?. is_none ( ) {
1006+ return error ( "RELEASE_NOT_FOUND" , "Release not found" , 404 ) ;
1007+ }
1008+ json_response (
1009+ & json ! ( { "admin" : actor, "release_id" : release_id, "history" : store:: release_history( & database, release_id) . await ?} ) ,
1010+ 200 ,
1011+ )
1012+ }
1013+
8311014async fn admin_release ( req : Request , ctx : RouteContext < ( ) > ) -> Result < Response > {
8321015 let release_id = param ( & ctx, "release_id" ) ;
8331016 if auth:: reviewer ( & req, & ctx. env ) ? {
@@ -1198,7 +1381,7 @@ async fn invalidate_release(mut req: Request, ctx: RouteContext<()>) -> Result<R
11981381 "release.validation_failed" ,
11991382 "release" ,
12001383 release_id,
1201- json ! ( { "developer_id" : value_str( & release, "registered_by" ) , "asset_id" : expected_asset_id, "package_id" : value_str( & release, "bundle_id" ) , "validation_attempt_id" : input. validation_attempt_id, "result" : "invalid" , "reason_code" : input. error_code} ) ,
1384+ json ! ( { "developer_id" : value_str( & release, "registered_by" ) , "asset_id" : expected_asset_id, "package_id" : value_str( & release, "bundle_id" ) , "validation_attempt_id" : input. validation_attempt_id, "result" : "invalid" , "reason_code" : input. error_code, "summary" : input . error_summary } ) ,
12021385 timestamp,
12031386 )
12041387 . await ?;
@@ -1382,7 +1565,7 @@ async fn reject_release(mut req: Request, ctx: RouteContext<()>) -> Result<Respo
13821565 "release.reject" ,
13831566 "release" ,
13841567 release_id,
1385- json ! ( { "developer_id" : value_str( & release, "registered_by" ) , "asset_id" : release. get( "github_asset_id" ) , "package_id" : value_str( & release, "bundle_id" ) , "result" : "rejected" , "reason_code" : input. reason_code} ) ,
1568+ json ! ( { "developer_id" : value_str( & release, "registered_by" ) , "asset_id" : release. get( "github_asset_id" ) , "package_id" : value_str( & release, "bundle_id" ) , "result" : "rejected" , "reason_code" : input. reason_code, "note" : input . note } ) ,
13861569 timestamp,
13871570 )
13881571 . await ?;
@@ -1945,9 +2128,35 @@ pub async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
19452128 list_developer_releases,
19462129 )
19472130 . post_async ( "/v1/developer/apps/:bundle_id/releases" , create_release)
2131+ . get_async (
2132+ "/v1/developer/apps/:bundle_id/history" ,
2133+ developer_app_history,
2134+ )
2135+ . get_async ( "/v1/developer/notifications" , developer_notifications)
2136+ . post_async (
2137+ "/v1/developer/notifications/read-all" ,
2138+ read_all_developer_notifications,
2139+ )
2140+ . post_async (
2141+ "/v1/developer/notifications/:notification_id/read" ,
2142+ read_developer_notification,
2143+ )
19482144 . get_async ( "/v1/admin/releases" , admin_releases)
2145+ . get_async ( "/v1/admin/notifications" , admin_notifications)
2146+ . post_async (
2147+ "/v1/admin/notifications/read-all" ,
2148+ read_all_admin_notifications,
2149+ )
2150+ . post_async (
2151+ "/v1/admin/notifications/:notification_id/read" ,
2152+ read_admin_notification,
2153+ )
19492154 . post_async ( "/v1/reviewer/releases/claim" , claim_next_release)
19502155 . get_async ( "/v1/admin/releases/:release_id" , admin_release)
2156+ . get_async (
2157+ "/v1/admin/releases/:release_id/history" ,
2158+ admin_release_history,
2159+ )
19512160 . post_async ( "/v1/admin/releases/:release_id/validate" , validate_release)
19522161 . post_async (
19532162 "/v1/admin/releases/:release_id/validation-failure" ,
@@ -2250,4 +2459,18 @@ mod tests {
22502459 assert ! ( production. contains( "validation_started_at<?3" ) ) ;
22512460 assert ! ( production. contains( "\" source\" : \" automatic_queue\" " ) ) ;
22522461 }
2462+
2463+ #[ test]
2464+ fn notification_and_history_routes_keep_developer_and_operator_scopes_separate ( ) {
2465+ let source = include_str ! ( "lib.rs" ) ;
2466+ let production = source. split ( "#[cfg(test)]" ) . next ( ) . unwrap_or_default ( ) ;
2467+ assert ! ( valid_notification_id( "audit_019f9d57" ) ) ;
2468+ assert ! ( !valid_notification_id( "../audit" ) ) ;
2469+ assert ! ( production. contains( "/v1/developer/notifications/:notification_id/read" ) ) ;
2470+ assert ! ( production. contains( "require_developer_actor(&req, &ctx.env)" ) ) ;
2471+ assert ! ( production. contains( "/v1/admin/notifications/:notification_id/read" ) ) ;
2472+ assert ! ( production. contains( "require_admin(&req, &ctx.env)" ) ) ;
2473+ assert ! ( production. contains( "/v1/developer/apps/:bundle_id/history" ) ) ;
2474+ assert ! ( production. contains( "/v1/admin/releases/:release_id/history" ) ) ;
2475+ }
22532476}
0 commit comments