@@ -18,6 +18,7 @@ use crate::networking;
1818use crate :: backup;
1919use crate :: agent:: { ClusterState , AgentMessage } ;
2020use crate :: auth:: SessionManager ;
21+ use crate :: appstore;
2122
2223mod console;
2324mod pve_console;
@@ -4717,6 +4718,86 @@ pub async fn mysql_dump(
47174718 }
47184719}
47194720
4721+ // ─── App Store ───
4722+
4723+ /// GET /api/appstore/apps?q=<query>&category=<cat> — list/search available apps
4724+ pub async fn appstore_list (
4725+ req : HttpRequest ,
4726+ state : web:: Data < AppState > ,
4727+ query : web:: Query < std:: collections:: HashMap < String , String > > ,
4728+ ) -> HttpResponse {
4729+ if let Err ( resp) = require_auth ( & req, & state) { return resp; }
4730+ let q = query. get ( "q" ) . map ( |s| s. as_str ( ) ) ;
4731+ let cat = query. get ( "category" ) . map ( |s| s. as_str ( ) ) ;
4732+ let apps = appstore:: list_apps ( q, cat) ;
4733+ HttpResponse :: Ok ( ) . json ( apps)
4734+ }
4735+
4736+ /// GET /api/appstore/apps/{id} — get app details
4737+ pub async fn appstore_get (
4738+ req : HttpRequest ,
4739+ state : web:: Data < AppState > ,
4740+ path : web:: Path < String > ,
4741+ ) -> HttpResponse {
4742+ if let Err ( resp) = require_auth ( & req, & state) { return resp; }
4743+ let id = path. into_inner ( ) ;
4744+ match appstore:: get_app ( & id) {
4745+ Some ( app) => HttpResponse :: Ok ( ) . json ( app) ,
4746+ None => HttpResponse :: NotFound ( ) . json ( serde_json:: json!( { "error" : format!( "App '{}' not found" , id) } ) ) ,
4747+ }
4748+ }
4749+
4750+ #[ derive( Deserialize ) ]
4751+ pub struct AppInstallRequest {
4752+ pub target : String , // "docker", "lxc", "bare"
4753+ pub container_name : String , // name for the container
4754+ #[ serde( default ) ]
4755+ pub inputs : std:: collections:: HashMap < String , String > , // user input values
4756+ }
4757+
4758+ /// POST /api/appstore/apps/{id}/install — install an app
4759+ pub async fn appstore_install (
4760+ req : HttpRequest ,
4761+ state : web:: Data < AppState > ,
4762+ path : web:: Path < String > ,
4763+ body : web:: Json < AppInstallRequest > ,
4764+ ) -> HttpResponse {
4765+ if let Err ( resp) = require_auth ( & req, & state) { return resp; }
4766+ let id = path. into_inner ( ) ;
4767+ let mut inputs = body. inputs . clone ( ) ;
4768+ // Inject CONTAINER_NAME for ${CONTAINER_NAME} substitution in manifests
4769+ inputs. insert ( "CONTAINER_NAME" . to_string ( ) , body. container_name . clone ( ) ) ;
4770+
4771+ match appstore:: install_app ( & id, & body. target , & body. container_name , & inputs) {
4772+ Ok ( msg) => HttpResponse :: Ok ( ) . json ( serde_json:: json!( { "message" : msg } ) ) ,
4773+ Err ( e) => HttpResponse :: InternalServerError ( ) . json ( serde_json:: json!( { "error" : e } ) ) ,
4774+ }
4775+ }
4776+
4777+ /// GET /api/appstore/installed — list installed apps
4778+ pub async fn appstore_installed (
4779+ req : HttpRequest ,
4780+ state : web:: Data < AppState > ,
4781+ ) -> HttpResponse {
4782+ if let Err ( resp) = require_auth ( & req, & state) { return resp; }
4783+ let apps = appstore:: list_installed_apps ( ) ;
4784+ HttpResponse :: Ok ( ) . json ( apps)
4785+ }
4786+
4787+ /// DELETE /api/appstore/installed/{id} — uninstall an app
4788+ pub async fn appstore_uninstall (
4789+ req : HttpRequest ,
4790+ state : web:: Data < AppState > ,
4791+ path : web:: Path < String > ,
4792+ ) -> HttpResponse {
4793+ if let Err ( resp) = require_auth ( & req, & state) { return resp; }
4794+ let install_id = path. into_inner ( ) ;
4795+ match appstore:: uninstall_app ( & install_id) {
4796+ Ok ( msg) => HttpResponse :: Ok ( ) . json ( serde_json:: json!( { "message" : msg } ) ) ,
4797+ Err ( e) => HttpResponse :: InternalServerError ( ) . json ( serde_json:: json!( { "error" : e } ) ) ,
4798+ }
4799+ }
4800+
47204801/// Configure all API routes
47214802pub fn configure ( cfg : & mut web:: ServiceConfig ) {
47224803 cfg
@@ -4892,6 +4973,12 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
48924973 . route ( "/api/wolfnet/used-ips" , web:: get ( ) . to ( wolfnet_used_ips_endpoint) )
48934974 // Geolocation proxy (ip-api.com is HTTP-only, browsers block mixed content on HTTPS pages)
48944975 . route ( "/api/geolocate" , web:: get ( ) . to ( geolocate) )
4976+ // App Store
4977+ . route ( "/api/appstore/apps" , web:: get ( ) . to ( appstore_list) )
4978+ . route ( "/api/appstore/apps/{id}" , web:: get ( ) . to ( appstore_get) )
4979+ . route ( "/api/appstore/apps/{id}/install" , web:: post ( ) . to ( appstore_install) )
4980+ . route ( "/api/appstore/installed" , web:: get ( ) . to ( appstore_installed) )
4981+ . route ( "/api/appstore/installed/{id}" , web:: delete ( ) . to ( appstore_uninstall) )
48954982 // System
48964983 . route ( "/api/config/export" , web:: get ( ) . to ( config_export) )
48974984 . route ( "/api/config/import" , web:: post ( ) . to ( config_import) )
0 commit comments