Skip to main content

tibba_model_token/
key.rs

1// Copyright 2026 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::{
16    Error, JsonSnafu, ModelListParams, Schema, SchemaAllowCreate, SchemaAllowEdit, SchemaType,
17    SchemaView, SqlxSnafu, format_datetime,
18};
19use serde::{Deserialize, Serialize};
20use snafu::ResultExt;
21use sqlx::FromRow;
22use sqlx::{Pool, Postgres, QueryBuilder};
23use std::collections::HashMap;
24use tibba_model::Model;
25use time::PrimitiveDateTime;
26
27type Result<T> = std::result::Result<T, Error>;
28
29#[derive(FromRow)]
30struct TokenKeySchema {
31    id: i64,
32    user_id: i64,
33    token: String,
34    name: String,
35    status: i16,
36    expired_at: Option<PrimitiveDateTime>,
37    created_by: i64,
38    created: PrimitiveDateTime,
39    modified: PrimitiveDateTime,
40}
41
42#[derive(Debug, Clone, Deserialize, Serialize)]
43pub struct TokenKey {
44    pub id: i64,
45    pub user_id: i64,
46    pub token: String,
47    pub name: String,
48    pub status: i16,
49    pub expired_at: Option<String>,
50    pub created_by: i64,
51    pub created: String,
52    pub modified: String,
53}
54
55impl From<TokenKeySchema> for TokenKey {
56    fn from(s: TokenKeySchema) -> Self {
57        Self {
58            id: s.id,
59            user_id: s.user_id,
60            token: s.token,
61            name: s.name,
62            status: s.status,
63            expired_at: s.expired_at.map(format_datetime),
64            created_by: s.created_by,
65            created: format_datetime(s.created),
66            modified: format_datetime(s.modified),
67        }
68    }
69}
70
71#[derive(Debug, Clone, Deserialize)]
72pub struct TokenKeyInsertParams {
73    pub user_id: i64,
74    pub name: String,
75    pub created_by: Option<i64>,
76}
77
78#[derive(Debug, Clone, Deserialize, Default)]
79pub struct TokenKeyUpdateParams {
80    pub name: Option<String>,
81    pub status: Option<i16>,
82    pub expired_at: Option<String>,
83}
84
85#[derive(Default)]
86pub struct TokenKeyModel {}
87
88impl TokenKeyModel {
89    /// 根据 token 字符串查询绑定的 user_id,供鉴权中间件使用。
90    /// 仅返回未删除、已启用、且未过期的记录。
91    pub async fn get_user_id_by_token(
92        &self,
93        pool: &Pool<Postgres>,
94        token: &str,
95    ) -> Result<Option<i64>> {
96        let result = sqlx::query_as::<_, (i64,)>(
97            r#"SELECT user_id FROM token_keys
98                WHERE token = $1
99                  AND status = 1
100                  AND deleted_at IS NULL
101                  AND (expired_at IS NULL OR expired_at > NOW())"#,
102        )
103        .bind(token)
104        .fetch_optional(pool)
105        .await
106        .context(SqlxSnafu)?;
107        Ok(result.map(|r| r.0))
108    }
109}
110
111impl Model for TokenKeyModel {
112    type Output = TokenKey;
113    fn new() -> Self {
114        Self::default()
115    }
116
117    async fn schema_view(&self, _pool: &Pool<Postgres>) -> SchemaView {
118        SchemaView {
119            schemas: vec![
120                Schema::new_id(),
121                Schema::new_user_search("user_id"),
122                Schema {
123                    name: "token".to_string(),
124                    category: SchemaType::String,
125                    read_only: true,
126                    auto_create: true,
127                    popover: true,
128                    ..Default::default()
129                },
130                Schema::new_name(),
131                Schema::new_status(),
132                Schema {
133                    name: "expired_at".to_string(),
134                    category: SchemaType::Date,
135                    ..Default::default()
136                },
137                Schema {
138                    name: "created_by".to_string(),
139                    category: SchemaType::Number,
140                    read_only: true,
141                    hidden: true,
142                    auto_create: true,
143                    ..Default::default()
144                },
145                Schema::new_created(),
146                Schema::new_modified(),
147            ],
148            allow_edit: SchemaAllowEdit {
149                roles: vec!["su".to_string(), "admin".to_string()],
150                ..Default::default()
151            },
152            allow_create: SchemaAllowCreate {
153                roles: vec!["su".to_string(), "admin".to_string()],
154                ..Default::default()
155            },
156        }
157    }
158
159    async fn insert(&self, pool: &Pool<Postgres>, mut data: serde_json::Value) -> Result<u64> {
160        // user_id 支持前端以字符串形式传入
161        if let Some(obj) = data.as_object_mut() {
162            if let Some(id_str) = obj.get("user_id").and_then(|v| v.as_str()) {
163                if let Ok(id) = id_str.parse::<i64>() {
164                    obj.insert("user_id".to_string(), id.into());
165                }
166            }
167        }
168        let p: TokenKeyInsertParams = serde_json::from_value(data).context(JsonSnafu)?;
169        let token = uuid::Uuid::new_v4().to_string();
170        let row: (i64,) = sqlx::query_as(
171            r#"INSERT INTO token_keys (user_id, name, token, created_by)
172               VALUES ($1, $2, $3, $4) RETURNING id"#,
173        )
174        .bind(p.user_id)
175        .bind(&p.name)
176        .bind(&token)
177        .bind(p.created_by.unwrap_or(0))
178        .fetch_one(pool)
179        .await
180        .context(SqlxSnafu)?;
181        Ok(row.0 as u64)
182    }
183
184    async fn get_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<Option<Self::Output>> {
185        let result = sqlx::query_as::<_, TokenKeySchema>(
186            r#"SELECT * FROM token_keys WHERE id = $1 AND deleted_at IS NULL"#,
187        )
188        .bind(id as i64)
189        .fetch_optional(pool)
190        .await
191        .context(SqlxSnafu)?;
192        Ok(result.map(Into::into))
193    }
194
195    async fn update_by_id(
196        &self,
197        pool: &Pool<Postgres>,
198        id: u64,
199        data: serde_json::Value,
200    ) -> Result<()> {
201        let p: TokenKeyUpdateParams = serde_json::from_value(data).context(JsonSnafu)?;
202        let mut qb: QueryBuilder<Postgres> =
203            QueryBuilder::new("UPDATE token_keys SET modified = NOW()");
204        if let Some(name) = p.name {
205            qb.push(", name = ").push_bind(name);
206        }
207        if let Some(status) = p.status {
208            qb.push(", status = ").push_bind(status);
209        }
210        if let Some(expired_at) = p.expired_at {
211            if expired_at.is_empty() {
212                qb.push(", expired_at = NULL");
213            } else {
214                let dt = tibba_model::parse_primitive_datetime(&expired_at).map_err(|_| {
215                    Error::NotSupported {
216                        name: "invalid expired_at".to_string(),
217                    }
218                })?;
219                qb.push(", expired_at = ").push_bind(dt);
220            }
221        }
222        qb.push(" WHERE id = ").push_bind(id as i64);
223        qb.push(" AND deleted_at IS NULL");
224        qb.build().execute(pool).await.context(SqlxSnafu)?;
225        Ok(())
226    }
227
228    async fn delete_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<()> {
229        sqlx::query(
230            r#"UPDATE token_keys SET deleted_at = NOW(), modified = NOW() WHERE id = $1 AND deleted_at IS NULL"#,
231        )
232        .bind(id as i64)
233        .execute(pool)
234        .await
235        .context(SqlxSnafu)?;
236        Ok(())
237    }
238
239    async fn count(&self, pool: &Pool<Postgres>, params: &ModelListParams) -> Result<i64> {
240        let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("SELECT COUNT(*) FROM token_keys");
241        self.push_conditions(&mut qb, params)?;
242        let row: (i64,) = qb
243            .build_query_as()
244            .fetch_one(pool)
245            .await
246            .context(SqlxSnafu)?;
247        Ok(row.0)
248    }
249
250    async fn list(
251        &self,
252        pool: &Pool<Postgres>,
253        params: &ModelListParams,
254    ) -> Result<Vec<Self::Output>> {
255        let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("SELECT * FROM token_keys");
256        self.push_conditions(&mut qb, params)?;
257        params.push_pagination(&mut qb);
258        let rows = qb
259            .build_query_as::<TokenKeySchema>()
260            .fetch_all(pool)
261            .await
262            .context(SqlxSnafu)?;
263        Ok(rows.into_iter().map(Into::into).collect())
264    }
265
266    fn push_filter_conditions<'args>(
267        &self,
268        qb: &mut QueryBuilder<'args, Postgres>,
269        filters: &HashMap<String, String>,
270    ) -> Result<()> {
271        if let Some(user_id) = filters.get("user_id") {
272            if let Ok(v) = user_id.parse::<i64>() {
273                qb.push(" AND user_id = ").push_bind(v);
274            }
275        }
276        Ok(())
277    }
278}