feat: add admin creation entry switches
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
This commit is contained in:
@@ -18,6 +18,8 @@ use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value};
|
||||
use shared_contracts::admin::{
|
||||
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
|
||||
AdminUpsertCreationEntryTypeConfigRequest,
|
||||
AdminDatabaseOverviewPayload, AdminDatabaseTableListResponse, AdminDatabaseTableRowPayload,
|
||||
AdminDatabaseTableRowsQuery, AdminDatabaseTableRowsResponse, AdminDatabaseTableStatPayload,
|
||||
AdminDebugHeaderInput, AdminDebugHttpRequest, AdminDebugHttpResponse, AdminLoginRequest,
|
||||
@@ -194,6 +196,94 @@ pub async fn admin_list_database_table_rows(
|
||||
Ok(json_success_body(Some(&request_context), response))
|
||||
}
|
||||
|
||||
|
||||
pub async fn admin_get_creation_entry_config(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(_admin): Extension<AuthenticatedAdmin>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let config = state.get_creation_entry_config().await.map_err(map_admin_spacetime_error)?;
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
AdminCreationEntryConfigResponse {
|
||||
entries: config
|
||||
.creation_types
|
||||
.into_iter()
|
||||
.map(map_admin_creation_entry_type_config)
|
||||
.collect(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn admin_upsert_creation_entry_config(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(_admin): Extension<AuthenticatedAdmin>,
|
||||
Json(payload): Json<AdminUpsertCreationEntryTypeConfigRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let entry = validate_admin_creation_entry_config(payload)?;
|
||||
let config = state
|
||||
.upsert_creation_entry_type_config(entry)
|
||||
.await
|
||||
.map_err(map_admin_spacetime_error)?;
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
AdminCreationEntryConfigResponse {
|
||||
entries: config
|
||||
.creation_types
|
||||
.into_iter()
|
||||
.map(map_admin_creation_entry_type_config)
|
||||
.collect(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn map_admin_creation_entry_type_config(
|
||||
entry: shared_contracts::creation_entry_config::CreationEntryTypeResponse,
|
||||
) -> AdminCreationEntryTypeConfigPayload {
|
||||
AdminCreationEntryTypeConfigPayload {
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
subtitle: entry.subtitle,
|
||||
badge: entry.badge,
|
||||
image_src: entry.image_src,
|
||||
visible: entry.visible,
|
||||
open: entry.open,
|
||||
sort_order: entry.sort_order,
|
||||
updated_at_micros: entry.updated_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_admin_creation_entry_config(
|
||||
payload: AdminUpsertCreationEntryTypeConfigRequest,
|
||||
) -> Result<module_runtime::CreationEntryTypeAdminUpsertInput, AppError> {
|
||||
let id = payload.id.trim().to_string();
|
||||
if id.is_empty() {
|
||||
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("入口 ID 不能为空"));
|
||||
}
|
||||
let title = payload.title.trim().to_string();
|
||||
if title.is_empty() {
|
||||
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("入口标题不能为空"));
|
||||
}
|
||||
Ok(module_runtime::CreationEntryTypeAdminUpsertInput {
|
||||
id,
|
||||
title,
|
||||
subtitle: payload.subtitle.trim().to_string(),
|
||||
badge: payload.badge.trim().to_string(),
|
||||
image_src: payload.image_src.trim().to_string(),
|
||||
visible: payload.visible,
|
||||
open: payload.open,
|
||||
sort_order: payload.sort_order,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_admin_spacetime_error(error: spacetime_client::SpacetimeClientError) -> AppError {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(serde_json::json!({
|
||||
"provider": "spacetimedb",
|
||||
"message": error.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn require_admin_auth(
|
||||
State(state): State<AppState>,
|
||||
mut request: Request,
|
||||
@@ -1237,7 +1327,9 @@ mod tests {
|
||||
};
|
||||
use axum::{http::StatusCode, response::IntoResponse};
|
||||
use serde_json::json;
|
||||
use shared_contracts::admin::{AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery};
|
||||
use shared_contracts::admin::{
|
||||
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
|
||||
AdminUpsertCreationEntryTypeConfigRequest,AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery};
|
||||
|
||||
#[test]
|
||||
fn normalize_debug_path_rejects_absolute_url() {
|
||||
|
||||
@@ -15,8 +15,9 @@ use tracing::{Level, Span, error, info, info_span, warn};
|
||||
|
||||
use crate::{
|
||||
admin::{
|
||||
admin_debug_http, admin_list_database_table_rows, admin_list_database_tables,
|
||||
admin_list_tracking_events, admin_login, admin_me, admin_overview, require_admin_auth,
|
||||
admin_debug_http, admin_get_creation_entry_config, admin_list_database_table_rows,
|
||||
admin_list_database_tables, admin_list_tracking_events, admin_login, admin_me, admin_overview,
|
||||
admin_upsert_creation_entry_config, require_admin_auth,
|
||||
},
|
||||
ai_tasks::{
|
||||
append_ai_text_chunk, attach_ai_result_reference, cancel_ai_task, complete_ai_stage,
|
||||
@@ -225,6 +226,15 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_admin_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/admin/api/creation-entry/config",
|
||||
get(admin_get_creation_entry_config)
|
||||
.post(admin_upsert_creation_entry_config)
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_admin_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/admin/api/profile/redeem-codes",
|
||||
get(admin_list_profile_redeem_codes)
|
||||
|
||||
@@ -228,6 +228,27 @@ impl AppState {
|
||||
&self.refresh_cookie_config
|
||||
}
|
||||
|
||||
pub async fn upsert_creation_entry_type_config(
|
||||
&self,
|
||||
input: module_runtime::CreationEntryTypeAdminUpsertInput,
|
||||
) -> Result<CreationEntryConfigResponse, SpacetimeClientError> {
|
||||
match self
|
||||
.spacetime_client
|
||||
.upsert_creation_entry_type_config(input)
|
||||
.await
|
||||
{
|
||||
Ok(config) => {
|
||||
#[cfg(test)]
|
||||
self.cache_test_creation_entry_config(config.clone());
|
||||
Ok(config)
|
||||
}
|
||||
#[cfg(test)]
|
||||
Err(_) => Ok(self.read_test_creation_entry_config()),
|
||||
#[cfg(not(test))]
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_creation_entry_config(
|
||||
&self,
|
||||
) -> Result<CreationEntryConfigResponse, SpacetimeClientError> {
|
||||
|
||||
@@ -88,6 +88,19 @@ pub struct CreationEntryConfigSnapshot {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CreationEntryTypeAdminUpsertInput {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub badge: String,
|
||||
pub image_src: String,
|
||||
pub visible: bool,
|
||||
pub open: bool,
|
||||
pub sort_order: i32,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CreationEntryConfigProcedureResult {
|
||||
|
||||
@@ -10,6 +10,44 @@ pub struct AdminLoginRequest {
|
||||
}
|
||||
|
||||
// 登录成功后返回管理员访问令牌与基础会话信息。
|
||||
|
||||
|
||||
/// 后台创作入口开关列表响应。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminCreationEntryConfigResponse {
|
||||
pub entries: Vec<AdminCreationEntryTypeConfigPayload>,
|
||||
}
|
||||
|
||||
/// 后台单个创作入口开关配置。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminCreationEntryTypeConfigPayload {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub badge: String,
|
||||
pub image_src: String,
|
||||
pub visible: bool,
|
||||
pub open: bool,
|
||||
pub sort_order: i32,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
/// 后台保存创作入口开关配置请求。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminUpsertCreationEntryTypeConfigRequest {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub badge: String,
|
||||
pub image_src: String,
|
||||
pub visible: bool,
|
||||
pub open: bool,
|
||||
pub sort_order: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminLoginResponse {
|
||||
|
||||
@@ -46,6 +46,23 @@ impl From<module_assets::AssetHistoryListInput> for AssetHistoryListInput {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::CreationEntryTypeAdminUpsertInput>
|
||||
for CreationEntryTypeAdminUpsertInput
|
||||
{
|
||||
fn from(input: module_runtime::CreationEntryTypeAdminUpsertInput) -> Self {
|
||||
Self {
|
||||
id: input.id,
|
||||
title: input.title,
|
||||
subtitle: input.subtitle,
|
||||
badge: input.badge,
|
||||
image_src: input.image_src,
|
||||
visible: input.visible,
|
||||
open: input.open,
|
||||
sort_order: input.sort_order,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeSettingGetInput> for RuntimeSettingGetInput {
|
||||
fn from(input: module_runtime::RuntimeSettingGetInput) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct CreationEntryTypeAdminUpsertInput {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub badge: String,
|
||||
pub image_src: String,
|
||||
pub visible: bool,
|
||||
pub open: bool,
|
||||
pub sort_order: i32,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for CreationEntryTypeAdminUpsertInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -252,6 +252,7 @@ pub mod custom_world_works_list_result_type;
|
||||
pub mod creation_entry_config_procedure_result_type;
|
||||
pub mod creation_entry_config_snapshot_type;
|
||||
pub mod creation_entry_start_card_snapshot_type;
|
||||
pub mod creation_entry_type_admin_upsert_input_type;
|
||||
pub mod creation_entry_type_config_snapshot_type;
|
||||
pub mod creation_entry_type_modal_snapshot_type;
|
||||
pub mod database_migration_authorize_operator_input_type;
|
||||
@@ -758,6 +759,7 @@ pub mod update_square_hole_work_procedure;
|
||||
pub mod update_visual_novel_work_procedure;
|
||||
pub mod upsert_auth_store_snapshot_procedure;
|
||||
pub mod upsert_chapter_progression_and_return_procedure;
|
||||
pub mod upsert_creation_entry_type_config_procedure;
|
||||
pub mod upsert_chapter_progression_reducer;
|
||||
pub mod upsert_custom_world_agent_operation_progress_procedure;
|
||||
pub mod upsert_custom_world_profile_and_return_procedure;
|
||||
@@ -1054,6 +1056,7 @@ pub use custom_world_works_list_result_type::CustomWorldWorksListResult;
|
||||
pub use creation_entry_config_procedure_result_type::CreationEntryConfigProcedureResult;
|
||||
pub use creation_entry_config_snapshot_type::CreationEntryConfigSnapshot;
|
||||
pub use creation_entry_start_card_snapshot_type::CreationEntryStartCardSnapshot;
|
||||
pub use creation_entry_type_admin_upsert_input_type::CreationEntryTypeAdminUpsertInput;
|
||||
pub use creation_entry_type_config_snapshot_type::CreationEntryTypeSnapshot;
|
||||
pub use creation_entry_type_modal_snapshot_type::CreationEntryTypeModalSnapshot;
|
||||
pub use database_migration_authorize_operator_input_type::DatabaseMigrationAuthorizeOperatorInput;
|
||||
@@ -1560,6 +1563,7 @@ pub use update_square_hole_work_procedure::update_square_hole_work;
|
||||
pub use update_visual_novel_work_procedure::update_visual_novel_work;
|
||||
pub use upsert_auth_store_snapshot_procedure::upsert_auth_store_snapshot;
|
||||
pub use upsert_chapter_progression_and_return_procedure::upsert_chapter_progression_and_return;
|
||||
pub use upsert_creation_entry_type_config_procedure::upsert_creation_entry_type_config;
|
||||
pub use upsert_chapter_progression_reducer::upsert_chapter_progression;
|
||||
pub use upsert_custom_world_agent_operation_progress_procedure::upsert_custom_world_agent_operation_progress;
|
||||
pub use upsert_custom_world_profile_and_return_procedure::upsert_custom_world_profile_and_return;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::creation_entry_config_procedure_result_type::CreationEntryConfigProcedureResult;
|
||||
use super::creation_entry_type_admin_upsert_input_type::CreationEntryTypeAdminUpsertInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct UpsertCreationEntryTypeConfigArgs {
|
||||
pub input: CreationEntryTypeAdminUpsertInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for UpsertCreationEntryTypeConfigArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `upsert_creation_entry_type_config`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait upsert_creation_entry_type_config {
|
||||
fn upsert_creation_entry_type_config(&self, input: CreationEntryTypeAdminUpsertInput) {
|
||||
self.upsert_creation_entry_type_config_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn upsert_creation_entry_type_config_then(
|
||||
&self,
|
||||
input: CreationEntryTypeAdminUpsertInput,
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl upsert_creation_entry_type_config for super::RemoteProcedures {
|
||||
fn upsert_creation_entry_type_config_then(
|
||||
&self,
|
||||
input: CreationEntryTypeAdminUpsertInput,
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CreationEntryConfigProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, CreationEntryConfigProcedureResult>(
|
||||
"upsert_creation_entry_type_config",
|
||||
UpsertCreationEntryTypeConfigArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,25 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn upsert_creation_entry_type_config(
|
||||
&self,
|
||||
input: module_runtime::CreationEntryTypeAdminUpsertInput,
|
||||
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
|
||||
let procedure_input: CreationEntryTypeAdminUpsertInput = input.into();
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection.procedures().upsert_creation_entry_type_config_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_creation_entry_config_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_runtime_settings(
|
||||
&self,
|
||||
user_id: String,
|
||||
|
||||
@@ -48,6 +48,57 @@ pub fn get_creation_entry_config(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn upsert_creation_entry_type_config(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: CreationEntryTypeAdminUpsertInput,
|
||||
) -> CreationEntryConfigProcedureResult {
|
||||
match ctx.try_with_tx(|tx| upsert_creation_entry_type_config_in_tx(tx, input.clone())) {
|
||||
Ok(record) => CreationEntryConfigProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => CreationEntryConfigProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn upsert_creation_entry_type_config_in_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: CreationEntryTypeAdminUpsertInput,
|
||||
) -> Result<CreationEntryConfigSnapshot, String> {
|
||||
seed_creation_entry_config_if_missing(ctx);
|
||||
let now = ctx.timestamp;
|
||||
let id = input.id.trim().to_string();
|
||||
if id.is_empty() {
|
||||
return Err("入口 ID 不能为空".to_string());
|
||||
}
|
||||
if input.title.trim().is_empty() {
|
||||
return Err("入口标题不能为空".to_string());
|
||||
}
|
||||
let row = CreationEntryTypeConfig {
|
||||
id: id.clone(),
|
||||
title: input.title.trim().to_string(),
|
||||
subtitle: input.subtitle.trim().to_string(),
|
||||
badge: input.badge.trim().to_string(),
|
||||
image_src: input.image_src.trim().to_string(),
|
||||
visible: input.visible,
|
||||
open: input.open,
|
||||
sort_order: input.sort_order,
|
||||
updated_at: now,
|
||||
};
|
||||
if ctx.db.creation_entry_type_config().id().find(&id).is_some() {
|
||||
ctx.db.creation_entry_type_config().id().update(row);
|
||||
} else {
|
||||
ctx.db.creation_entry_type_config().insert(row);
|
||||
}
|
||||
get_or_seed_creation_entry_config_snapshot(ctx)
|
||||
}
|
||||
|
||||
fn get_or_seed_creation_entry_config_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
) -> Result<CreationEntryConfigSnapshot, String> {
|
||||
|
||||
Reference in New Issue
Block a user