iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0
Software Development

Rust Web API 從零開始系列 第 21

Day21 - 整合認證信寄送

  • 分享至 

  • xImage
  •  

昨天建立了用來寄送email的組件,接下來就要把它放到API中了,首先要對資料表進行異動,除了增加一個欄位用來紀錄訂閱的狀態以外,也要增加一張表用來紀錄認證信所需的token,當訂戶打開驗證連結時使用token來修改訂閱狀態:

    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_type(
                Type::create()
                    .as_enum(SubscriptionStatus::Table)
                    .values(SubscriptionStatus::iter().skip(1))
                    .to_owned(),
            )
            .await?;

        manager
            .alter_table(
                Table::alter()
                    .table(Subscriptions::Table)
                    .add_column(
                        ColumnDef::new(Subscriptions::Status)
                            .enumeration(
                                SubscriptionStatus::Table,
                                SubscriptionStatus::iter().skip(1),
                            )
                            .not_null()
                            .default("inactive"),
                    )
                    .to_owned(),
            )
            .await?;

        manager
            .create_table(
                Table::create()
                    .table(SubscriptionTokens::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(SubscriptionTokens::SubscriptionToken)
                            .text()
                            .not_null()
                            .primary_key(),
                    )
                    .col(
                        ColumnDef::new(SubscriptionTokens::SubscriberId)
                            .uuid()
                            .not_null(),
                    )
                    .to_owned(),
            )
            .await?;

        manager
            .create_foreign_key(
                ForeignKey::create()
                    .from(SubscriptionTokens::Table, SubscriptionTokens::SubscriberId)
                    .to(Subscriptions::Table, Subscriptions::Id)
                    .on_delete(ForeignKeyAction::Cascade)
                    .on_update(ForeignKeyAction::Cascade)
                    .to_owned(),
            )
            .await?;

        Ok(())
    }

第一段的create_type是用來在postgres中增加一個enum,這是seaORM中對於posrgres特有的支援,可以用更語意化的方式紀錄狀態,也對該欄位的內容進行約束。
第二段alter_table中,為了指定狀態欄位使用前面的enum,需要使用enumeration方法,其中的SubscriptionStatus::iter().skip(1)也可以改成寫死的Vec
最後在建立新表的時候,則需要注意外鍵關聯的建立。當腳本完成之後就可以進行遷移了。

紀錄token

接下來則是回過頭修改subscribe handler,在紀錄訂閱資料的同時要關聯認證用的token:

pub async fn subscribe(state: State<AppState>, Form(data): Form<NewSubscriber>) -> Result<StatusCode,StatusCode> {

    let new_subscriber = Subscriber::try_from(data)
        .map_err(|_| StatusCode::BAD_REQUEST)?;

    let subscriber = subscriptions::ActiveModel {
        id: Set(Uuid::new_v4()),
        email: Set(new_subscriber.email.as_ref().to_owned()),
        name: Set(new_subscriber.name.as_ref().to_owned()),
        subscribed_at: Set(chrono::Utc::now().into()),
        ..Default::default()
    }
    .insert(&state.database)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    let subscription_token = subscription_tokens::ActiveModel {
        subscriber_id: Set(subscriber.id),
        subscription_token: Set(Uuid::new_v4().to_string()),
    }
    .insert(&state.database)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    Ok(StatusCode::Ok)
}

但是目前的寫法是兩次執行的,也就是說可能會發生新增了訂閱紀錄,卻沒有產生認證token,因此需要把它用交易包起來:

pub async fn subscribe(state: State<AppState>, Form(data): Form<NewSubscriber>) -> Result<StatusCode,StatusCode> {

    let new_subscriber = Subscriber::try_from(data)
        .map_err(|_| StatusCode::BAD_REQUEST)?;
    
    //// 交易開始
    let txn = state.database.begin().await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let subscriber = subscriptions::ActiveModel {
        id: Set(Uuid::new_v4()),
        email: Set(new_subscriber.email.as_ref().to_owned()),
        name: Set(new_subscriber.name.as_ref().to_owned()),
        subscribed_at: Set(chrono::Utc::now().into()),
        ..Default::default()
    }
    .insert(&state.database)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    let subscription_token = subscription_tokens::ActiveModel {
        subscriber_id: Set(subscriber.id),
        subscription_token: Set(Uuid::new_v4().to_string()),
    }
    .insert(&state.database)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    //// 交易結束
    txn.commit().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    Ok(StatusCode::Ok)
}

寄送認證信

我們要來利用前一天做好的email_client,昨天提到了這個結構需要在整個應用程式中重複利用,所以就把它加入AppState中,要調整的地方有configuration.rs用於讀取第三方服務的設定、Application.rs中於路由的部份加入email_client,最後再調整subscribe handler,由於我們希望訂閱紀錄、寄送信件都要一次做完,所以要把寄信加入交易的區間內。

pub async fn subscribe(state: State<AppState>, Form(data): Form<NewSubscriber>) -> Result<StatusCode,StatusCode> {

    let new_subscriber = Subscriber::try_from(data)
        .map_err(|_| StatusCode::BAD_REQUEST)?;
        
    let txn = state.database.begin().await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let subscriber = subscriptions::ActiveModel {
        id: Set(Uuid::new_v4()),
        email: Set(new_subscriber.email.as_ref().to_owned()),
        name: Set(new_subscriber.name.as_ref().to_owned()),
        subscribed_at: Set(chrono::Utc::now().into()),
        ..Default::default()
    }
    .insert(&state.database)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    let subscription_token = subscription_tokens::ActiveModel {
        subscriber_id: Set(subscriber.id),
        subscription_token: Set(Uuid::new_v4().to_string()),
    }
    .insert(&state.database)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    //// 取得email內容
    let html_body = get_email_content(&state, subscription_token);
    
    //// 寄送信件,因為不需要回傳值所以使用_
    _ = &state
        .email_client
        .send_email(&new_subscriber.email, "Welcome!", &html_body)
        .await?;
    
    txn.commit().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
    
    Ok(StatusCode::Ok)
}

完整的實做可以參考這裡


上一篇
Day20 - 使用reqwest串接寄信服務
下一篇
Day22 - 訂閱確認,再論SeaORM中的ActiveModel
系列文
Rust Web API 從零開始30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言