Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .ai/REST_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1571,4 +1571,18 @@ GET /api/v1/groups/{id}, POST /api/v1/groups/join 응답의 `data` 필드:
- `deleted`: briefing_subscriptions row 삭제 (notification_hour NULL 또는 Pro 상실)
- 에러: 401 (X-Scheduler-Secret 불일치)

*마지막 업데이트: 2026-04-26*
### POST /api/v1/internal/live-activity/jobs/{job_id}/dispatch

- 설명: Cloud Tasks가 예약 시각에 호출하는 Live Activity job dispatch. 지정된 `job_id` 하나만 claim 후 처리
- 인증: `X-Scheduler-Secret` 헤더 (SCHEDULER_SECRET 환경변수와 대조). 불일치 시 401
- 요청 바디: 없음
- 응답 200:
```json
{
"processed": true
}
```
- `processed`: 해당 job을 claim/처리했으면 true. 이미 처리됐거나 due가 아니면 false
- 에러: 401 (X-Scheduler-Secret 불일치), 500 (SCHEDULER_SECRET 미설정 또는 처리 실패)

*마지막 업데이트: 2026-05-21*
7 changes: 6 additions & 1 deletion .github/workflows/deploy-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
apns_environment: ${{ steps.env.outputs.apns_environment }}
apns_bundle_id: ${{ steps.env.outputs.apns_bundle_id }}
gcs_bucket: ${{ steps.env.outputs.gcs_bucket }}
rust_api_url: ${{ steps.env.outputs.rust_api_url }}
env_label: ${{ steps.env.outputs.env_label }}
emoji: ${{ steps.env.outputs.emoji }}

Expand All @@ -40,6 +41,7 @@ jobs:
APNS_ENV="development"
APNS_BUNDLE="com.promiso.dev"
GCS_BUCKET="promiso-dev-media"
RUST_API_URL="https://promiso-api-7z5hrxvm5q-du.a.run.app"
EMOJI="🟢"
LABEL="Dev"
;;
Expand All @@ -48,6 +50,7 @@ jobs:
APNS_ENV="development"
APNS_BUNDLE="com.promiso.stage"
GCS_BUCKET="promiso-stage-media"
RUST_API_URL="https://promiso-api-2re4udjdkq-du.a.run.app"
EMOJI="🟡"
LABEL="Stage"
;;
Expand All @@ -56,6 +59,7 @@ jobs:
APNS_ENV="production"
APNS_BUNDLE="com.promiso"
GCS_BUCKET="promiso-prod-media"
RUST_API_URL="https://promiso-api-maen7botya-du.a.run.app"
EMOJI="🚀"
LABEL="Production"
;;
Expand All @@ -71,6 +75,7 @@ jobs:
echo "apns_environment=$APNS_ENV"
echo "apns_bundle_id=$APNS_BUNDLE"
echo "gcs_bucket=$GCS_BUCKET"
echo "rust_api_url=$RUST_API_URL"
echo "env_label=$LABEL"
echo "emoji=$EMOJI"
} >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -184,7 +189,7 @@ jobs:
--project ${{ needs.setup.outputs.project_id }} \
--region asia-northeast3 \
--allow-unauthenticated \
--set-env-vars "FIREBASE_PROJECT_ID=${{ needs.setup.outputs.project_id }},RUST_LOG=info,APNS_KEY_ID=4CHBHPYY75,APNS_TEAM_ID=BAC795627G,APNS_BUNDLE_ID=${{ needs.setup.outputs.apns_bundle_id }},APNS_ENVIRONMENT=${{ needs.setup.outputs.apns_environment }},GCS_UPLOAD_BUCKET=${{ needs.setup.outputs.gcs_bucket }}" \
--set-env-vars "FIREBASE_PROJECT_ID=${{ needs.setup.outputs.project_id }},RUST_LOG=info,APNS_KEY_ID=4CHBHPYY75,APNS_TEAM_ID=BAC795627G,APNS_BUNDLE_ID=${{ needs.setup.outputs.apns_bundle_id }},APNS_ENVIRONMENT=${{ needs.setup.outputs.apns_environment }},GCS_UPLOAD_BUCKET=${{ needs.setup.outputs.gcs_bucket }},LIVE_ACTIVITY_TASK_PROJECT_ID=${{ needs.setup.outputs.project_id }},LIVE_ACTIVITY_TASK_LOCATION=asia-northeast3,LIVE_ACTIVITY_TASK_QUEUE=live-activity-jobs,LIVE_ACTIVITY_TASK_TARGET_BASE_URL=${{ needs.setup.outputs.rust_api_url }}" \
--set-secrets "DATABASE_URL=DATABASE_URL:latest,DATABASE_POOL_URL=DATABASE_POOL_URL:latest,AUTH_JWT_SECRET=AUTH_JWT_SECRET:latest,FIREBASE_SERVICE_ACCOUNT_JSON=FIREBASE_SERVICE_ACCOUNT_JSON:latest,GEMINI_API_KEY=GEMINI_API_KEY:latest,WIDGET_JWT_SECRET=WIDGET_JWT_SECRET:latest,APNS_AUTH_KEY=APNS_AUTH_KEY:latest,KAKAO_REST_API_KEY=KAKAO_REST_API_KEY:latest,ODSAY_API_KEY=ODSAY_API_KEY:latest,SCHEDULER_SECRET=SCHEDULER_SECRET:latest,KMA_API_KEY=KMA_API_KEY:latest" \
--quiet

Expand Down
9 changes: 9 additions & 0 deletions infra/rust-backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,14 @@ APNS_AUTH_KEY_PATH=/absolute/path/to/AuthKey_ABC123XYZ.p8
# optional override; default is derived from FIREBASE_PROJECT_ID
# APNS_BUNDLE_ID=com.promiso.dev
WIDGET_JWT_SECRET=replace-with-widget-jwt-secret
SCHEDULER_SECRET=replace-with-scheduler-secret
# Live Activity Cloud Tasks dispatch
# optional override; default is derived from FIREBASE_PROJECT_ID
# LIVE_ACTIVITY_TASK_PROJECT_ID=promiso-dev
# optional override
# LIVE_ACTIVITY_TASK_LOCATION=asia-northeast3
# optional override
# LIVE_ACTIVITY_TASK_QUEUE=live-activity-jobs
LIVE_ACTIVITY_TASK_TARGET_BASE_URL=https://promiso-api-xxxxx.a.run.app
GEMINI_API_KEY=replace-with-gemini-api-key
RUST_LOG=info
16 changes: 16 additions & 0 deletions infra/rust-backend/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ pub struct Config {
pub widget_jwt_secret: Option<String>,
pub odsay_api_key: Option<String>,
pub kakao_rest_api_key: Option<String>,
pub scheduler_secret: Option<String>,
pub live_activity_task_project_id: String,
pub live_activity_task_location: String,
pub live_activity_task_queue: String,
pub live_activity_task_target_base_url: Option<String>,
}

impl Config {
Expand Down Expand Up @@ -77,6 +82,17 @@ impl Config {
widget_jwt_secret: std::env::var("WIDGET_JWT_SECRET").ok(),
odsay_api_key: std::env::var("ODSAY_API_KEY").ok(),
kakao_rest_api_key: std::env::var("KAKAO_REST_API_KEY").ok(),
scheduler_secret: std::env::var("SCHEDULER_SECRET").ok(),
live_activity_task_project_id: std::env::var("LIVE_ACTIVITY_TASK_PROJECT_ID")
.unwrap_or_else(|_| {
std::env::var("FIREBASE_PROJECT_ID").expect("FIREBASE_PROJECT_ID must be set")
}),
live_activity_task_location: std::env::var("LIVE_ACTIVITY_TASK_LOCATION")
.unwrap_or_else(|_| "asia-northeast3".to_string()),
live_activity_task_queue: std::env::var("LIVE_ACTIVITY_TASK_QUEUE")
.unwrap_or_else(|_| "live-activity-jobs".to_string()),
live_activity_task_target_base_url: std::env::var("LIVE_ACTIVITY_TASK_TARGET_BASE_URL")
.ok(),
}
}
}
8 changes: 0 additions & 8 deletions infra/rust-backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use promiso_backend::config::Config;
use promiso_backend::push::{build_live_activity_sender, build_push_sender};
use promiso_backend::routes;
use promiso_backend::services::live_activity_service;

use tokio::net::TcpListener;
use tracing_subscriber::EnvFilter;
Expand Down Expand Up @@ -31,12 +29,6 @@ async fn main() {
.await
.expect("Failed to connect to database pool");

let _live_activity_worker = live_activity_service::spawn_worker(
pool.clone(),
build_live_activity_sender(&config),
build_push_sender(&config),
);

let app = routes::create_router(pool, &config);

// [::]:port → IPv4 + IPv6 동시 바인딩 (iOS 시뮬레이터 호환)
Expand Down
64 changes: 59 additions & 5 deletions infra/rust-backend/src/routes/internal.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
use std::sync::Arc;

use axum::extract::State;
use axum::extract::{Path, State};
use axum::http::HeaderMap;
use axum::routing::post;
use axum::{Extension, Json, Router};
use chrono::Utc;
use serde::Serialize;
use sqlx::PgPool;
use uuid::Uuid;

use crate::errors::AppError;
use crate::models::live_activity::LiveActivitySender;
use crate::models::notification::PushSender;
use crate::services::briefing_scheduler_service::{
self, dispatch_due_briefings, verify_scheduler_secret,
};
use crate::services::live_activity_service::{self, LiveActivityJobScheduler};

pub fn router() -> Router<PgPool> {
Router::new().route(
"/api/v1/internal/briefing/dispatch",
post(dispatch_briefings_handler),
)
Router::new()
.route(
"/api/v1/internal/briefing/dispatch",
post(dispatch_briefings_handler),
)
.route(
"/api/v1/internal/live-activity/jobs/{job_id}/dispatch",
post(dispatch_live_activity_job_handler),
)
}

#[derive(Debug, Serialize)]
Expand All @@ -27,6 +35,12 @@ struct DispatchResponse {
summary: briefing_scheduler_service::DispatchSummary,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct LiveActivityJobDispatchResponse {
processed: bool,
}

/// POST /api/v1/internal/briefing/dispatch
///
/// 인증: X-Scheduler-Secret 헤더 vs SCHEDULER_SECRET 환경변수
Expand Down Expand Up @@ -62,3 +76,43 @@ async fn dispatch_briefings_handler(
// 5. 응답
Ok(Json(DispatchResponse { summary }))
}

async fn dispatch_live_activity_job_handler(
State(pool): State<PgPool>,
Extension(push_sender): Extension<Arc<dyn PushSender>>,
Extension(live_activity_sender): Extension<Arc<dyn LiveActivitySender>>,
Extension(live_activity_job_scheduler): Extension<Arc<dyn LiveActivityJobScheduler>>,
headers: HeaderMap,
Path(job_id): Path<Uuid>,
) -> Result<Json<LiveActivityJobDispatchResponse>, AppError> {
let provided = headers
.get("x-scheduler-secret")
.and_then(|v| v.to_str().ok())
.unwrap_or("");

let expected = match std::env::var("SCHEDULER_SECRET") {
Ok(v) if !v.is_empty() => v,
_ => {
return Err(AppError::Internal(
"SCHEDULER_SECRET not configured".to_string(),
))
}
};

if !verify_scheduler_secret(provided, &expected) {
return Err(AppError::Unauthorized("Invalid scheduler secret".into()));
}

let result = live_activity_service::dispatch_live_activity_job_with_scheduler(
&pool,
live_activity_sender.as_ref(),
push_sender.as_ref(),
live_activity_job_scheduler.as_ref(),
job_id,
)
.await?;

Ok(Json(LiveActivityJobDispatchResponse {
processed: result.processed,
}))
}
12 changes: 10 additions & 2 deletions infra/rust-backend/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use crate::middleware::auth::{require_auth, ServerAuth, WidgetAuth};
use crate::push::{build_live_activity_sender, build_push_sender};
use crate::routes::transportation::TransportationKeys;
use crate::services::app_store_service::{RealAppStoreVerifier, SharedAppStoreVerifier};
use crate::services::live_activity_service::build_live_activity_job_scheduler;
use crate::services::provider_verifier::{RealProviderVerifier, SharedProviderVerifier};
use crate::services::storage_service::GcsUploadSigner;

Expand All @@ -40,8 +41,10 @@ pub fn create_router(pool: PgPool, config: &Config) -> Router {
let widget_auth = WidgetAuth::new(config.widget_jwt_secret.clone());
let push_sender = build_push_sender(config);
let live_activity_sender = build_live_activity_sender(config);
let live_activity_job_scheduler = build_live_activity_job_scheduler(config);
let public_push_sender = push_sender.clone();
let public_live_activity_sender = live_activity_sender.clone();
let public_live_activity_job_scheduler = live_activity_job_scheduler.clone();
let app_store_verifier: SharedAppStoreVerifier = Arc::new(RealAppStoreVerifier::new(config));
let provider_verifier: SharedProviderVerifier = Arc::new(RealProviderVerifier::new(config));
let gcs_upload_signer = match GcsUploadSigner::from_config(config) {
Expand Down Expand Up @@ -72,6 +75,7 @@ pub fn create_router(pool: PgPool, config: &Config) -> Router {
.merge(emoji::router())
.layer(axum::Extension(app_store_verifier.clone()))
.layer(axum::Extension(live_activity_sender.clone()))
.layer(axum::Extension(live_activity_job_scheduler.clone()))
.layer(axum::Extension(gcs_upload_signer.clone()))
.layer(axum::Extension(push_sender))
.layer(axum::Extension(transportation_keys))
Expand All @@ -82,10 +86,14 @@ pub fn create_router(pool: PgPool, config: &Config) -> Router {
.merge(subscriptions::public_router())
.merge(widget::widget_snapshot_router())
.layer(axum::Extension(app_store_verifier))
.layer(axum::Extension(public_live_activity_sender))
.layer(axum::Extension(public_live_activity_sender.clone()))
.layer(axum::Extension(public_live_activity_job_scheduler.clone()))
.layer(axum::Extension(public_push_sender.clone()));

let internal_routes = internal::router().layer(axum::Extension(public_push_sender));
let internal_routes = internal::router()
.layer(axum::Extension(public_push_sender))
.layer(axum::Extension(public_live_activity_sender))
.layer(axum::Extension(public_live_activity_job_scheduler));

Router::new()
.merge(health::router()) // /health — 인증 불필요
Expand Down
Loading
Loading