mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-21 06:51:24 +02:00
feat: profile templates
This commit is contained in:
1 parent
3f5b573ead
commit
2cc91ed250
29 files changed
+2430
-62
No files matched your search
+205
-1
@@ -274,6 +274,33 @@ impl From<crate::profile_statuses::ProfileStatus> for ApiStatusResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
struct TemplateRequest {
|
||||
/// 1 to 60 characters, unique (case-insensitive).
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
settings: crate::profile_templates::TemplateSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
struct CreateFromTemplateRequest {
|
||||
/// 1 to 100.
|
||||
count: u32,
|
||||
/// One profile takes this name; several take it followed by numbers that
|
||||
/// continue after the highest in use. Defaults to the template's name
|
||||
/// prefix, else its name.
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
struct ApiBulkCreateResponse {
|
||||
/// The profiles made, in order.
|
||||
profiles: Vec<ApiProfile>,
|
||||
/// Why creation stopped before `count` (an error code JSON), or `null`.
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
struct StatusRequest {
|
||||
/// 1 to 40 characters, unique (case-insensitive).
|
||||
@@ -745,6 +772,11 @@ struct ImportProxiesResponse {
|
||||
update_status,
|
||||
delete_status,
|
||||
bulk_edit_profiles,
|
||||
get_templates,
|
||||
create_template,
|
||||
update_template,
|
||||
delete_template,
|
||||
create_profiles_from_template,
|
||||
get_tags,
|
||||
get_proxies,
|
||||
get_proxy,
|
||||
@@ -795,6 +827,11 @@ struct ImportProxiesResponse {
|
||||
StatusRequest,
|
||||
BulkEditRequest,
|
||||
BulkEditResponse,
|
||||
TemplateRequest,
|
||||
CreateFromTemplateRequest,
|
||||
ApiBulkCreateResponse,
|
||||
crate::profile_templates::ProfileTemplate,
|
||||
crate::profile_templates::TemplateSettings,
|
||||
crate::profile::patch::ProfilePatch,
|
||||
ApiProxyResponse,
|
||||
crate::proxy_rotation::RotationResult,
|
||||
@@ -892,6 +929,7 @@ struct ImportProxiesResponse {
|
||||
(name = "profiles", description = "Profile management endpoints"),
|
||||
(name = "groups", description = "Group management endpoints"),
|
||||
(name = "statuses", description = "Profile status management endpoints"),
|
||||
(name = "templates", description = "Profile templates and creating profiles from them"),
|
||||
(name = "tags", description = "Tag management endpoints"),
|
||||
(name = "proxies", description = "Proxy management endpoints"),
|
||||
(name = "vpns", description = "VPN management endpoints"),
|
||||
@@ -1087,6 +1125,9 @@ fn build_v1_router() -> Router<ApiServerState> {
|
||||
.routes(routes!(get_statuses, create_status))
|
||||
.routes(routes!(update_status, delete_status))
|
||||
.routes(routes!(bulk_edit_profiles))
|
||||
.routes(routes!(get_templates, create_template))
|
||||
.routes(routes!(update_template, delete_template))
|
||||
.routes(routes!(create_profiles_from_template))
|
||||
.routes(routes!(get_tags))
|
||||
.routes(routes!(get_proxies, create_proxy))
|
||||
.routes(routes!(import_proxies_api))
|
||||
@@ -1399,7 +1440,7 @@ fn manager_error_response(err: impl std::fmt::Display) -> (StatusCode, String) {
|
||||
if let Some(code) = value.get("code").and_then(|c| c.as_str()) {
|
||||
let status = if code.ends_with("_NOT_FOUND") {
|
||||
StatusCode::NOT_FOUND
|
||||
} else if LAUNCH_CONFLICT_CODES.contains(&code) {
|
||||
} else if LAUNCH_CONFLICT_CODES.contains(&code) || code == "BULK_CREATE_BUSY" {
|
||||
// Someone or something else holds this profile: another team member, a
|
||||
// browser already open, or a remote session whose work has not been
|
||||
// pulled back yet. All of them are "try again later", not "your request
|
||||
@@ -2310,6 +2351,139 @@ async fn bulk_edit_profiles(
|
||||
.map_err(manager_error_response)
|
||||
}
|
||||
|
||||
// API Handlers - Templates
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/templates",
|
||||
responses(
|
||||
(status = 200, description = "Every profile template, in their order", body = Vec<crate::profile_templates::ProfileTemplate>),
|
||||
(status = 401, description = "Unauthorized")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
tag = "templates"
|
||||
)]
|
||||
async fn get_templates() -> Json<Vec<crate::profile_templates::ProfileTemplate>> {
|
||||
Json(crate::profile_templates::list())
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/templates",
|
||||
request_body = TemplateRequest,
|
||||
responses(
|
||||
(status = 200, description = "Template created. Device fields in `wayfern_config` are dropped: every profile made from it gets its own device", body = crate::profile_templates::ProfileTemplate),
|
||||
(status = 400, description = "Empty, too long or duplicate name, an invalid value, both a proxy and a VPN, or 100 templates already"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "A status, group or extension group does not exist")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
tag = "templates"
|
||||
)]
|
||||
async fn create_template(
|
||||
Json(request): Json<TemplateRequest>,
|
||||
) -> Result<Json<crate::profile_templates::ProfileTemplate>, (StatusCode, String)> {
|
||||
crate::profile_templates::create(&request.name, request.settings)
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(manager_error_response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/v1/templates/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Template ID")
|
||||
),
|
||||
request_body = TemplateRequest,
|
||||
responses(
|
||||
(status = 200, description = "Template replaced", body = crate::profile_templates::ProfileTemplate),
|
||||
(status = 400, description = "Empty, too long or duplicate name, an invalid value, or both a proxy and a VPN"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Template, status, group or extension group not found")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
tag = "templates"
|
||||
)]
|
||||
async fn update_template(
|
||||
Path(id): Path<String>,
|
||||
Json(request): Json<TemplateRequest>,
|
||||
) -> Result<Json<crate::profile_templates::ProfileTemplate>, (StatusCode, String)> {
|
||||
crate::profile_templates::update(&id, &request.name, request.settings)
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(manager_error_response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/templates/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Template ID")
|
||||
),
|
||||
responses(
|
||||
(status = 204, description = "Template deleted; profiles made from it keep their settings"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Template not found")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
tag = "templates"
|
||||
)]
|
||||
async fn delete_template(Path(id): Path<String>) -> Result<StatusCode, (StatusCode, String)> {
|
||||
crate::profile_templates::delete(&id)
|
||||
.map(|()| StatusCode::NO_CONTENT)
|
||||
.map_err(manager_error_response)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/templates/{id}/profiles",
|
||||
params(
|
||||
("id" = String, Path, description = "Template ID")
|
||||
),
|
||||
request_body = CreateFromTemplateRequest,
|
||||
responses(
|
||||
(status = 200, description = "Profiles made with the newest downloaded Wayfern, each with its own device. When `error` is set, creation stopped early and `profiles` lists what was made", body = ApiBulkCreateResponse),
|
||||
(status = 400, description = "Count outside 1 to 100, empty name, a name in use, or a proxy or VPN that does not work"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 402, description = "The template's fingerprint OS needs a paid plan"),
|
||||
(status = 404, description = "Template not found"),
|
||||
(status = 409, description = "Another bulk creation is running"),
|
||||
(status = 429, description = "The hourly profile allowance is used up; nothing was made")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
tag = "templates"
|
||||
)]
|
||||
async fn create_profiles_from_template(
|
||||
State(state): State<ApiServerState>,
|
||||
Path(id): Path<String>,
|
||||
Json(request): Json<CreateFromTemplateRequest>,
|
||||
) -> Result<Json<ApiBulkCreateResponse>, (StatusCode, String)> {
|
||||
crate::profile_templates::create_from_template(
|
||||
&state.app_handle,
|
||||
&id,
|
||||
request.name.as_deref(),
|
||||
request.count,
|
||||
)
|
||||
.await
|
||||
.map(|result| {
|
||||
Json(ApiBulkCreateResponse {
|
||||
profiles: result.created.iter().map(ApiProfile::from).collect(),
|
||||
error: result.error,
|
||||
})
|
||||
})
|
||||
.map_err(manager_error_response)
|
||||
}
|
||||
|
||||
// API Handlers - Tags
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -5991,6 +6165,36 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openapi_documents_bulk_edit_and_templates() {
|
||||
let spec = serde_json::to_value(ApiDoc::openapi()).expect("spec serializes");
|
||||
for path in [
|
||||
"/v1/profiles/bulk-edit",
|
||||
"/v1/templates",
|
||||
"/v1/templates/{id}",
|
||||
"/v1/templates/{id}/profiles",
|
||||
] {
|
||||
assert!(
|
||||
spec["paths"][path].is_object(),
|
||||
"{path} must be in the spec"
|
||||
);
|
||||
}
|
||||
let patch = &spec["components"]["schemas"]["ProfilePatch"];
|
||||
assert!(patch["properties"]["status_id"].is_object());
|
||||
assert!(
|
||||
schema_required(&spec, "ProfilePatch").is_empty(),
|
||||
"every patch field is optional"
|
||||
);
|
||||
assert!(
|
||||
schema_required(&spec, "TemplateSettings").is_empty(),
|
||||
"every template setting is optional"
|
||||
);
|
||||
assert!(!schema_required(&spec, "CreateFromTemplateRequest")
|
||||
.iter()
|
||||
.any(|f| f == "name"));
|
||||
assert!(spec["paths"]["/v1/templates/{id}/profiles"]["post"]["responses"]["429"].is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openapi_documents_the_profile_dns_server() {
|
||||
let spec = serde_json::to_value(ApiDoc::openapi()).expect("spec serializes");
|
||||
|
||||
Reference in new issue
Block a user