Skip to main content

kellnr_db/database/
mod.rs

1mod operations;
2pub mod test_utils;
3
4use std::cmp::max;
5use std::collections::{BTreeMap, HashMap};
6use std::path::Path;
7
8use chrono::{DateTime, Utc};
9use kellnr_common::crate_data::{CrateData, CrateRegistryDep, CrateVersionData};
10use kellnr_common::crate_dependents::{CrateDependent, CrateDependents};
11use kellnr_common::crate_overview::CrateOverview;
12use kellnr_common::cratesio_prefetch_msg::{CratesioPrefetchMsg, UpdateData};
13use kellnr_common::index_metadata::{IndexDep, IndexMetadata};
14use kellnr_common::normalized_name::NormalizedName;
15use kellnr_common::original_name::OriginalName;
16use kellnr_common::prefetch::Prefetch;
17use kellnr_common::publish_metadata::PublishMetadata;
18use kellnr_common::version::{Version, latest_version};
19use kellnr_common::webhook::{Webhook, WebhookEvent, WebhookQueue};
20use kellnr_entity::prelude::*;
21use kellnr_entity::{
22    auth_token, crate_author, crate_author_to_crate, crate_category, crate_category_to_crate,
23    crate_dependency, crate_group, crate_index, crate_keyword, crate_keyword_to_crate, crate_meta,
24    crate_user, cratesio_crate, cratesio_index, cratesio_meta, doc_queue, group, group_user, krate,
25    oauth2_identity, oauth2_state, owner, session, toolchain, toolchain_component,
26    toolchain_target, user, webhook, webhook_queue,
27};
28use kellnr_migration::iden::{
29    AuthTokenIden, CrateIden, CrateMetaIden, CratesIoIden, CratesIoMetaIden, GroupIden,
30};
31use kellnr_settings::constants::OAUTH2_STATE_MAX_AGE;
32use sea_orm::entity::prelude::Uuid;
33use sea_orm::prelude::async_trait::async_trait;
34use sea_orm::query::{QueryOrder, QuerySelect, TransactionTrait};
35use sea_orm::sea_query::{
36    Alias, Cond, Condition, Expr, Func, Iden, JoinType, LikeExpr, Order, Query, SelectStatement,
37    UnionType,
38};
39use sea_orm::{
40    ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, ExprTrait,
41    FromQueryResult, ModelTrait, QueryFilter, RelationTrait, Set, Statement,
42};
43use tracing::warn;
44
45use crate::error::DbError;
46use crate::password::{PwdVerification, hash_pwd, hash_token, verify_pwd};
47use crate::provider::{
48    ChannelInfo, CrateSort, CrateTagFilter, CrateVisibility, CratesioVersionMeta, DbResult,
49    OAuth2StateData, PrefetchState, SessionInfo, ToolchainComponentInfo, ToolchainTargetInfo,
50    ToolchainWithTargets,
51};
52use crate::tables::init_database;
53use crate::{
54    AuthToken, ConString, CrateMeta, CrateSummary, DbProvider, DocQueueEntry, Group, User,
55};
56
57const DB_DATE_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
58
59/// How often the last use of an auth token is written at most.
60const TOKEN_LAST_USED_INTERVAL: chrono::TimeDelta = chrono::TimeDelta::hours(1);
61
62/// Timestamp (in [`DB_DATE_FORMAT`]) before which an `OAuth2` state is expired.
63fn oauth2_state_expiry_cutoff() -> String {
64    let max_age =
65        chrono::Duration::from_std(OAUTH2_STATE_MAX_AGE).expect("constant fits chrono::Duration");
66    (Utc::now() - max_age).format(DB_DATE_FORMAT).to_string()
67}
68
69/// Escape the LIKE wildcards `%` and `_` (and the escape character itself) in a
70/// user-supplied search term so it is matched literally. Callers wrap the result
71/// in `%...%` and pair it with `ESCAPE '\'`. Without this, a search for `%`
72/// matches every row and turns the search endpoint into a cheap `DoS`.
73fn escape_like_pattern(input: &str) -> String {
74    input
75        .replace('\\', "\\\\")
76        .replace('%', "\\%")
77        .replace('_', "\\_")
78}
79
80/// Upper bound on the configured session age. Clamping to this keeps cutoff
81/// arithmetic well within `chrono::Duration`/`DateTime` range, avoiding
82/// overflow panics for absurd configuration values.
83const MAX_SESSION_AGE: std::time::Duration = std::time::Duration::from_hours(876_000);
84
85/// Resolve a configured session age into the internal expiry duration.
86///
87/// A value of zero means sessions never expire ([`None`]). Any positive value
88/// is clamped to [`MAX_SESSION_AGE`] so cutoff calculations cannot overflow.
89fn session_age_to_duration(d: std::time::Duration) -> Option<chrono::Duration> {
90    if d.is_zero() {
91        return None;
92    }
93    let clamped = d.min(MAX_SESSION_AGE);
94    // `clamped` is at most MAX_SESSION_AGE, which fits comfortably in a
95    // `chrono::Duration`, so the conversion cannot fail.
96    Some(chrono::Duration::from_std(clamped).expect("clamped session age fits chrono::Duration"))
97}
98
99#[derive(Debug, PartialEq, FromQueryResult)]
100struct CountResult {
101    count: Option<i64>,
102}
103
104pub struct Database {
105    db_con: DatabaseConnection,
106    /// Maximum lifetime of a session before it is treated as expired, or
107    /// `None` if sessions never expire (configured age of zero).
108    session_age: Option<chrono::Duration>,
109}
110
111impl Database {
112    pub async fn new(con: &ConString, max_con: u32) -> Result<Self, DbError> {
113        let db_con = init_database(con, max_con)
114            .await
115            .map_err(|e| DbError::InitializationError(e.to_string()))?;
116
117        if operations::no_user_exists(&db_con).await? {
118            operations::insert_admin_credentials(&db_con, con).await?;
119        }
120
121        Ok(Self {
122            db_con,
123            session_age: session_age_to_duration(con.session_age()),
124        })
125    }
126
127    /// Timestamp (in [`DB_DATE_FORMAT`]) before which a session is expired, or
128    /// `None` when sessions never expire. A session whose `created` value is
129    /// older than the returned cutoff should not validate.
130    fn session_expiry_cutoff(&self) -> Option<String> {
131        self.session_age
132            .map(|age| (Utc::now() - age).format(DB_DATE_FORMAT).to_string())
133    }
134
135    /// Replaces the legacy SHA-256 hash of `user` with an argon2 hash of `pwd`.
136    /// The update only applies while the stored hash is still the legacy one,
137    /// so a password changed concurrently is never overwritten.
138    async fn upgrade_legacy_pwd_hash(&self, user: &User, pwd: &str) -> DbResult<()> {
139        let hashed = hash_pwd(pwd).await;
140        user::Entity::update_many()
141            .col_expr(user::Column::Pwd, Expr::value(hashed))
142            .col_expr(user::Column::Salt, Expr::value(String::new()))
143            .filter(user::Column::Id.eq(i64::from(user.id)))
144            .filter(user::Column::Pwd.eq(user.pwd.as_str()))
145            .exec(&self.db_con)
146            .await?;
147        Ok(())
148    }
149
150    /// Looks up a user by name; returns the entity model or [`DbError::UserNotFound`].
151    async fn get_user_model(&self, name: &str) -> DbResult<user::Model> {
152        user::Entity::find()
153            .filter(user::Column::Name.eq(name))
154            .one(&self.db_con)
155            .await?
156            .ok_or_else(|| DbError::UserNotFound(name.to_owned()))
157    }
158
159    /// Looks up a krate by normalized name; returns the entity model or [`DbError::CrateNotFound`].
160    async fn get_krate_model(&self, crate_name: &NormalizedName) -> DbResult<krate::Model> {
161        krate::Entity::find()
162            .filter(krate::Column::Name.eq(crate_name))
163            .one(&self.db_con)
164            .await?
165            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))
166    }
167
168    /// Looks up a group by name; returns the entity model or [`DbError::GroupNotFound`].
169    async fn get_group_model(&self, name: &str) -> DbResult<group::Model> {
170        group::Entity::find()
171            .filter(group::Column::Name.eq(name))
172            .one(&self.db_con)
173            .await?
174            .ok_or_else(|| DbError::GroupNotFound(name.to_owned()))
175    }
176
177    /// Loads targets (with their components) for a toolchain and returns a `ToolchainWithTargets`.
178    async fn toolchain_with_targets(&self, tc: toolchain::Model) -> DbResult<ToolchainWithTargets> {
179        let targets = toolchain_target::Entity::find()
180            .filter(toolchain_target::Column::ToolchainFk.eq(tc.id))
181            .all(&self.db_con)
182            .await?;
183
184        let mut target_infos = Vec::with_capacity(targets.len());
185        for t in targets {
186            let components = toolchain_component::Entity::find()
187                .filter(toolchain_component::Column::ToolchainTargetFk.eq(t.id))
188                .all(&self.db_con)
189                .await?;
190            target_infos.push(ToolchainTargetInfo {
191                id: t.id,
192                target: t.target,
193                storage_path: t.storage_path,
194                hash: t.hash,
195                size: t.size,
196                status: t.status,
197                components: components
198                    .into_iter()
199                    .map(ToolchainComponentInfo::from)
200                    .collect(),
201            });
202        }
203
204        Ok(ToolchainWithTargets {
205            id: tc.id,
206            name: tc.name,
207            version: tc.version,
208            date: tc.date,
209            channel: tc.channel,
210            created: tc.created,
211            targets: target_infos,
212        })
213    }
214
215    /// Looks up a crate index by name and version; returns the entity model or [`DbError::CrateIndexNotFound`].
216    async fn get_crate_index_model(
217        &self,
218        crate_name: &NormalizedName,
219        version: &Version,
220    ) -> DbResult<crate_index::Model> {
221        crate_index::Entity::find()
222            .filter(crate_index::Column::Name.eq(crate_name))
223            .filter(crate_index::Column::Vers.eq(version))
224            .one(&self.db_con)
225            .await?
226            .ok_or_else(|| DbError::CrateIndexNotFound(crate_name.to_string(), version.to_string()))
227    }
228
229    /// Looks up a toolchain by name and version; returns `None` if not found.
230    async fn get_toolchain_by_name_version(
231        &self,
232        name: &str,
233        version: &str,
234    ) -> DbResult<Option<toolchain::Model>> {
235        toolchain::Entity::find()
236            .filter(toolchain::Column::Name.eq(name))
237            .filter(toolchain::Column::Version.eq(version))
238            .one(&self.db_con)
239            .await
240            .map_err(Into::into)
241    }
242
243    /// Looks up a `crate_group` by crate name and group name; returns `None` if not found.
244    async fn get_crate_group_by_name_and_group(
245        &self,
246        crate_name: &NormalizedName,
247        group: &str,
248    ) -> DbResult<Option<crate_group::Model>> {
249        crate_group::Entity::find()
250            .join(JoinType::InnerJoin, crate_group::Relation::Krate.def())
251            .join(JoinType::InnerJoin, crate_group::Relation::Group.def())
252            .filter(
253                Cond::all()
254                    .add(krate::Column::Name.eq(crate_name))
255                    .add(group::Column::Name.eq(group)),
256            )
257            .one(&self.db_con)
258            .await
259            .map_err(Into::into)
260    }
261
262    /// Looks up a `group_user` by group name and user name; returns `None` if not found.
263    async fn get_group_user_by_group_and_user(
264        &self,
265        group_name: &str,
266        user: &str,
267    ) -> DbResult<Option<group_user::Model>> {
268        group_user::Entity::find()
269            .join(JoinType::InnerJoin, group_user::Relation::Group.def())
270            .join(JoinType::InnerJoin, group_user::Relation::User.def())
271            .filter(
272                Cond::all()
273                    .add(group::Column::Name.eq(group_name))
274                    .add(user::Column::Name.eq(user)),
275            )
276            .one(&self.db_con)
277            .await
278            .map_err(Into::into)
279    }
280
281    /// Looks up a `crate_user` by crate name and user name; returns `None` if not found.
282    async fn get_crate_user_by_crate_and_user(
283        &self,
284        crate_name: &NormalizedName,
285        user: &str,
286    ) -> DbResult<Option<crate_user::Model>> {
287        crate_user::Entity::find()
288            .join(JoinType::InnerJoin, crate_user::Relation::Krate.def())
289            .join(JoinType::InnerJoin, crate_user::Relation::User.def())
290            .filter(
291                Cond::all()
292                    .add(krate::Column::Name.eq(crate_name))
293                    .add(user::Column::Name.eq(user)),
294            )
295            .one(&self.db_con)
296            .await
297            .map_err(Into::into)
298    }
299
300    /// Adds an optional `where` clause for case-insensitive matching of name and
301    /// description.
302    ///
303    /// Case folding is only reliable for ASCII. The pattern is lowercased by
304    /// Rust, which is Unicode-aware, while the columns are lowercased by the
305    /// database: `PostgreSQL` folds according to the locale, but `SQLite`
306    /// ships without ICU and folds ASCII only. A description containing
307    /// "ÄPFEL" therefore stays uppercase on `SQLite` and never matches the
308    /// lowercased pattern, for any spelling of the query. Crate names are
309    /// unaffected, since `OriginalName` restricts them to ASCII.
310    fn add_filter_by_name_and_description(
311        mut query: SelectStatement,
312        contains: Option<&str>,
313        name: Expr,
314        description: Expr,
315    ) -> SelectStatement {
316        if let Some(contains) = contains {
317            let pattern = format!("%{}%", escape_like_pattern(contains)).to_lowercase();
318            query.and_where(
319                Func::lower(name)
320                    .like(LikeExpr::new(&pattern).escape('\\'))
321                    .or(Func::lower(description).like(LikeExpr::new(&pattern).escape('\\'))),
322            );
323        }
324        query
325    }
326
327    /// Sort key for search results: exact name matches first, then names that
328    /// start with the query, then names that contain it, then matches in the
329    /// description only. Without a query every row gets the same key.
330    fn search_rank(contains: Option<&str>, name: &Expr) -> Expr {
331        let Some(contains) = contains else {
332            return Expr::val(0);
333        };
334        let query = contains.to_lowercase();
335        let escaped = escape_like_pattern(contains).to_lowercase();
336        let lower_name = || Func::lower(name.clone());
337        Expr::case(lower_name().eq(query), 0)
338            .case(
339                lower_name().like(LikeExpr::new(format!("{escaped}%")).escape('\\')),
340                1,
341            )
342            .case(
343                lower_name().like(LikeExpr::new(format!("%{escaped}%")).escape('\\')),
344                2,
345            )
346            .finally(3)
347            .into()
348    }
349
350    /// Joins the given SELECT statement against the crate table, restricts it to
351    /// the latest version of every crate and applies the optional name filter.
352    fn select_from_crates(mut query: SelectStatement, contains: Option<&str>) -> SelectStatement {
353        query
354            .inner_join(
355                CrateIden::Table,
356                Expr::col((CrateMetaIden::Table, CrateMetaIden::CrateFk))
357                    .equals((CrateIden::Table, CrateIden::Id)),
358            )
359            .and_where(
360                Expr::col((CrateMetaIden::Table, CrateMetaIden::Version))
361                    .equals((CrateIden::Table, CrateIden::MaxVersion)),
362            );
363
364        Self::add_filter_by_name_and_description(
365            query,
366            contains,
367            Expr::col((CrateIden::Table, CrateIden::OriginalName)),
368            Expr::col((CrateIden::Table, CrateIden::Description)),
369        )
370    }
371
372    /// Joins the given SELECT statement against the crates.io cache table,
373    /// restricts it to the latest version of every crate and applies the
374    /// optional name filter.
375    fn select_from_cache(mut query: SelectStatement, contains: Option<&str>) -> SelectStatement {
376        query
377            .inner_join(
378                CratesIoIden::Table,
379                Expr::col((CratesIoMetaIden::Table, CratesIoMetaIden::CratesIoFk))
380                    .equals((CratesIoIden::Table, CratesIoIden::Id)),
381            )
382            .and_where(
383                Expr::col((CratesIoMetaIden::Table, CratesIoMetaIden::Version))
384                    .equals((CratesIoIden::Table, CratesIoIden::MaxVersion)),
385            );
386
387        Self::add_filter_by_name_and_description(
388            query,
389            contains,
390            Expr::col((CratesIoIden::Table, CratesIoIden::OriginalName)),
391            Expr::col((CratesIoIden::Table, CratesIoIden::Description)),
392        )
393    }
394
395    /// Condition on the crate table that hides the download-restricted crates
396    /// `visibility` does not grant access to. `None` if nothing is hidden.
397    fn crate_visibility_condition(visibility: &CrateVisibility) -> Option<Condition> {
398        let unrestricted = Expr::col((CrateIden::Table, CrateIden::RestrictedDownload)).eq(false);
399        match visibility {
400            CrateVisibility::All => None,
401            CrateVisibility::Unrestricted => Some(Cond::all().add(unrestricted)),
402            CrateVisibility::User(user) => {
403                let crate_id = || Expr::col((CrateIden::Table, CrateIden::Id));
404                Some(
405                    Cond::any()
406                        .add(unrestricted)
407                        .add(crate_id().in_subquery(Self::owned_crate_ids(user)))
408                        .add(crate_id().in_subquery(Self::crate_user_crate_ids(user)))
409                        .add(crate_id().in_subquery(Self::crate_group_crate_ids(user))),
410                )
411            }
412        }
413    }
414
415    /// Condition on the crate table that keeps the crates with the keyword
416    /// and category of `tags`. `None` if `tags` filters nothing.
417    fn crate_tag_condition(tags: &CrateTagFilter) -> Option<Condition> {
418        if tags.is_empty() {
419            return None;
420        }
421        let crate_id = || Expr::col((CrateIden::Table, CrateIden::Id));
422        let mut condition = Cond::all();
423        if let Some(keyword) = &tags.keyword {
424            condition = condition.add(crate_id().in_subquery(Self::keyword_crate_ids(keyword)));
425        }
426        if let Some(category) = &tags.category {
427            condition = condition.add(crate_id().in_subquery(Self::category_crate_ids(category)));
428        }
429        Some(condition)
430    }
431
432    fn keyword_crate_ids(keyword: &str) -> SelectStatement {
433        Query::select()
434            .column((
435                crate_keyword_to_crate::Entity,
436                crate_keyword_to_crate::Column::CrateFk,
437            ))
438            .from(crate_keyword_to_crate::Entity)
439            .inner_join(
440                crate_keyword::Entity,
441                Expr::col((crate_keyword::Entity, crate_keyword::Column::Id)).equals((
442                    crate_keyword_to_crate::Entity,
443                    crate_keyword_to_crate::Column::KeywordFk,
444                )),
445            )
446            .and_where(
447                Func::lower(Expr::col((
448                    crate_keyword::Entity,
449                    crate_keyword::Column::Keyword,
450                )))
451                .eq(keyword.to_lowercase()),
452            )
453            .to_owned()
454    }
455
456    fn category_crate_ids(category: &str) -> SelectStatement {
457        Query::select()
458            .column((
459                crate_category_to_crate::Entity,
460                crate_category_to_crate::Column::CrateFk,
461            ))
462            .from(crate_category_to_crate::Entity)
463            .inner_join(
464                crate_category::Entity,
465                Expr::col((crate_category::Entity, crate_category::Column::Id)).equals((
466                    crate_category_to_crate::Entity,
467                    crate_category_to_crate::Column::CategoryFk,
468                )),
469            )
470            .and_where(
471                Func::lower(Expr::col((
472                    crate_category::Entity,
473                    crate_category::Column::Category,
474                )))
475                .eq(category.to_lowercase()),
476            )
477            .to_owned()
478    }
479
480    fn user_id_by_name(user: &str) -> SelectStatement {
481        Query::select()
482            .column((user::Entity, user::Column::Id))
483            .from(user::Entity)
484            .and_where(Expr::col((user::Entity, user::Column::Name)).eq(user))
485            .to_owned()
486    }
487
488    fn owned_crate_ids(user: &str) -> SelectStatement {
489        Query::select()
490            .column((owner::Entity, owner::Column::CrateFk))
491            .from(owner::Entity)
492            .and_where(
493                Expr::col((owner::Entity, owner::Column::UserFk))
494                    .in_subquery(Self::user_id_by_name(user)),
495            )
496            .to_owned()
497    }
498
499    fn crate_user_crate_ids(user: &str) -> SelectStatement {
500        Query::select()
501            .column((crate_user::Entity, crate_user::Column::CrateFk))
502            .from(crate_user::Entity)
503            .and_where(
504                Expr::col((crate_user::Entity, crate_user::Column::UserFk))
505                    .in_subquery(Self::user_id_by_name(user)),
506            )
507            .to_owned()
508    }
509
510    fn crate_group_crate_ids(user: &str) -> SelectStatement {
511        Query::select()
512            .column((crate_group::Entity, crate_group::Column::CrateFk))
513            .from(crate_group::Entity)
514            .inner_join(
515                group_user::Entity,
516                Expr::col((group_user::Entity, group_user::Column::GroupFk))
517                    .equals((crate_group::Entity, crate_group::Column::GroupFk)),
518            )
519            .and_where(
520                Expr::col((group_user::Entity, group_user::Column::UserFk))
521                    .in_subquery(Self::user_id_by_name(user)),
522            )
523            .to_owned()
524    }
525
526    /// Shared implementation for crate overview listing: optional name filter,
527    /// optional keyword and category filter, optional limit/offset, cache union.
528    async fn query_crates(
529        &self,
530        contains: Option<&str>,
531        limit_offset: Option<(u64, u64)>,
532        cache: bool,
533        visibility: &CrateVisibility,
534        sort: CrateSort,
535        tags: &CrateTagFilter,
536    ) -> DbResult<Vec<CrateOverview>> {
537        /// Creates a SELECT statement for a given set of fields.
538        fn select_fields(fields: &[(Expr, &str)]) -> SelectStatement {
539            let mut query = Query::select();
540            for (col, alias) in fields {
541                query.expr_as(col.clone(), Alias::new(*alias));
542            }
543            query
544        }
545
546        let mut query = select_fields(&[
547            (Expr::col(CrateIden::OriginalName), "name"),
548            (Expr::col(CrateIden::MaxVersion), "version"),
549            (Expr::col(CrateIden::LastUpdated), "date"),
550            (Expr::col(CrateIden::TotalDownloads), "total_downloads"),
551            (Expr::col(CrateIden::Description), "description"),
552            (Expr::col(CrateMetaIden::Documentation), "documentation"),
553            (Expr::col(CrateIden::Repository), "repository"),
554            (Expr::cust("false"), "is_cache"),
555            (
556                Self::search_rank(
557                    contains,
558                    &Expr::col((CrateIden::Table, CrateIden::OriginalName)),
559                ),
560                "search_rank",
561            ),
562        ]);
563        query.from(CrateMetaIden::Table);
564        query = Self::select_from_crates(query, contains);
565        if let Some(condition) = Self::crate_visibility_condition(visibility) {
566            query.cond_where(condition);
567        }
568        if let Some(condition) = Self::crate_tag_condition(tags) {
569            query.cond_where(condition);
570        }
571
572        // UNION with cached crates, which have no keywords or categories
573        if cache && tags.is_empty() {
574            let mut query2 = select_fields(&[
575                (Expr::col(CratesIoIden::OriginalName), "name"),
576                (Expr::col(CratesIoIden::MaxVersion), "version"),
577                (Expr::col(CratesIoIden::LastModified), "date"),
578                (Expr::col(CratesIoIden::TotalDownloads), "total_downloads"),
579                (Expr::col(CratesIoIden::Description), "description"),
580                (Expr::col(CratesIoMetaIden::Documentation), "documentation"),
581                // The proxy does not store the repository of crates.io crates
582                (Expr::cust("NULL"), "repository"),
583                (Expr::cust("true"), "is_cache"),
584                (
585                    Self::search_rank(
586                        contains,
587                        &Expr::col((CratesIoIden::Table, CratesIoIden::OriginalName)),
588                    ),
589                    "search_rank",
590                ),
591            ]);
592            query2.from(CratesIoMetaIden::Table);
593            query2 = Self::select_from_cache(query2, contains);
594
595            query.union(UnionType::All, query2);
596        }
597
598        // Ordering and optional limit/offset. The name breaks ties, so paging
599        // stays stable within one rank, date or download count.
600        match sort {
601            CrateSort::Name => query.order_by(Alias::new("search_rank"), Order::Asc),
602            // Dates are stored as "YYYY-MM-DD HH:MM:SS", so they sort as text.
603            CrateSort::Updated => query.order_by(Alias::new("date"), Order::Desc),
604            CrateSort::Downloads => query.order_by(Alias::new("total_downloads"), Order::Desc),
605        };
606        query.order_by(Alias::new("name"), Order::Asc);
607        if let Some((limit, offset)) = limit_offset {
608            query.limit(limit).offset(offset);
609        }
610
611        let stmt = self.db_con.get_database_backend().build(&query);
612
613        CrateOverview::find_by_statement(stmt)
614            .all(&self.db_con)
615            .await
616            .map_err(DbError::from)
617    }
618
619    async fn count_crates(
620        &self,
621        contains: Option<&str>,
622        cache: bool,
623        visibility: &CrateVisibility,
624        tags: &CrateTagFilter,
625    ) -> DbResult<u64> {
626        let mut query = Query::select();
627        query.expr_as(
628            Expr::col((CrateIden::Table, CrateIden::Id)).count(),
629            Alias::new("count"),
630        );
631        query.from(CrateMetaIden::Table);
632        query = Self::select_from_crates(query, contains);
633        if let Some(condition) = Self::crate_visibility_condition(visibility) {
634            query.cond_where(condition);
635        }
636        if let Some(condition) = Self::crate_tag_condition(tags) {
637            query.cond_where(condition);
638        }
639
640        let count = self
641            .count_by_statement(
642                self.db_con.get_database_backend().build(&query),
643                DbError::FailedToCountCrates,
644            )
645            .await?;
646
647        let cache_count = if cache && tags.is_empty() {
648            let mut query2 = Query::select();
649            query2.expr_as(
650                Expr::col((CratesIoIden::Table, CratesIoIden::Id)).count(),
651                Alias::new("count"),
652            );
653            query2.from(CratesIoMetaIden::Table);
654            query2 = Self::select_from_cache(query2, contains);
655
656            self.count_by_statement(
657                self.db_con.get_database_backend().build(&query2),
658                DbError::FailedToCountCrates,
659            )
660            .await?
661        } else {
662            0
663        };
664
665        Ok(count + cache_count)
666    }
667
668    /// Executes a count query `SELECT COUNT(id_column) FROM table` and returns the count as u64.
669    async fn count<T>(&self, table: T, id_column: T, error: DbError) -> DbResult<u64>
670    where
671        T: Iden + Copy,
672    {
673        let statement = self.db_con.get_database_backend().build(
674            Query::select()
675                .expr_as(Expr::col((table, id_column)).count(), Alias::new("count"))
676                .from(table),
677        );
678        self.count_by_statement(statement, error).await
679    }
680
681    /// Runs a statement that selects a single `count` column and returns it as u64.
682    async fn count_by_statement(&self, statement: Statement, error: DbError) -> DbResult<u64> {
683        let Some(result) = CountResult::find_by_statement(statement)
684            .one(&self.db_con)
685            .await?
686        else {
687            return Err(error);
688        };
689        result
690            .count
691            .and_then(|v| u64::try_from(v).ok())
692            .ok_or(error)
693    }
694
695    async fn get_webhook_model(&self, id: &str) -> DbResult<webhook::Model> {
696        webhook::Entity::find()
697            .filter(webhook::Column::Id.eq(
698                TryInto::<Uuid>::try_into(id).map_err(|_| DbError::InvalidId(id.to_string()))?,
699            ))
700            .one(&self.db_con)
701            .await?
702            .ok_or(DbError::WebhookNotFound)
703    }
704
705    async fn get_webhook_queue_model(&self, id: &str) -> DbResult<webhook_queue::Model> {
706        webhook_queue::Entity::find()
707            .filter(webhook_queue::Column::Id.eq(
708                TryInto::<Uuid>::try_into(id).map_err(|_| DbError::InvalidId(id.to_string()))?,
709            ))
710            .one(&self.db_con)
711            .await?
712            .ok_or(DbError::WebhookNotFound)
713    }
714
715    async fn get_owner_by_crate_and_user(
716        &self,
717        crate_name: &str,
718        owner_name: &str,
719    ) -> DbResult<Option<owner::Model>> {
720        owner::Entity::find()
721            .join(JoinType::InnerJoin, owner::Relation::Krate.def())
722            .join(JoinType::InnerJoin, owner::Relation::User.def())
723            .filter(
724                Cond::all()
725                    .add(krate::Column::Name.eq(crate_name))
726                    .add(user::Column::Name.eq(owner_name)),
727            )
728            .one(&self.db_con)
729            .await
730            .map_err(Into::into)
731    }
732
733    async fn delete_or_not_found<M, A>(&self, model: Option<M>, err: DbError) -> DbResult<()>
734    where
735        M: ModelTrait + sea_orm::IntoActiveModel<A>,
736        A: ActiveModelTrait<Entity = M::Entity> + sea_orm::ActiveModelBehavior + Send,
737    {
738        model.ok_or(err)?.delete(&self.db_con).await?;
739        Ok(())
740    }
741}
742
743fn webhook_model_to_obj(w: webhook::Model) -> DbResult<Webhook> {
744    let event = w
745        .event
746        .as_str()
747        .try_into()
748        .map_err(|_| DbError::InvalidWebhookEvent(w.event))?;
749    Ok(Webhook {
750        id: Some(w.id.into()),
751        name: w.name,
752        event,
753        callback_url: w.callback_url,
754        secret: w.secret,
755    })
756}
757
758/// Generates an `add_*` method that links two entities via a join table.
759///
760/// # Parameters
761/// - `$method`: The method name to generate (e.g., `add_crate_user_impl`)
762/// - `$relation_entity`: The join table entity module (e.g., `crate_user`)
763/// - `$insert_entity`: The Sea ORM Entity type for insertion (e.g., `CrateUser`)
764/// - `$fk1`: The foreign key field for the first entity
765/// - `$fk2`: The foreign key field for the second entity
766macro_rules! impl_add_relation {
767    ($method:ident, $relation_entity:ident, $insert_entity:ident, $fk1:ident, $fk2:ident) => {
768        async fn $method(&self, fk1: i64, fk2: i64) -> DbResult<()> {
769            let model = $relation_entity::ActiveModel {
770                $fk1: Set(fk1),
771                $fk2: Set(fk2),
772                ..Default::default()
773            };
774            $insert_entity::insert(model).exec(&self.db_con).await?;
775            Ok(())
776        }
777    };
778}
779
780impl Database {
781    impl_add_relation!(
782        add_crate_user_impl,
783        crate_user,
784        CrateUser,
785        crate_fk,
786        user_fk
787    );
788    impl_add_relation!(
789        add_crate_group_impl,
790        crate_group,
791        CrateGroup,
792        crate_fk,
793        group_fk
794    );
795    impl_add_relation!(
796        add_group_user_impl,
797        group_user,
798        GroupUser,
799        group_fk,
800        user_fk
801    );
802    impl_add_relation!(add_owner_impl, owner, Owner, crate_fk, user_fk);
803}
804
805#[async_trait]
806impl DbProvider for Database {
807    async fn get_total_unique_cached_crates(&self) -> DbResult<u64> {
808        self.count(
809            CratesIoIden::Table,
810            CratesIoIden::Id,
811            DbError::FailedToCountCrates,
812        )
813        .await
814    }
815
816    async fn get_total_cached_crate_versions(&self) -> DbResult<u64> {
817        self.count(
818            CratesIoMetaIden::Table,
819            CratesIoMetaIden::Id,
820            DbError::FailedToCountCrateVersions,
821        )
822        .await
823    }
824
825    async fn get_total_cached_downloads(&self) -> DbResult<u64> {
826        #[derive(FromQueryResult)]
827        struct Model {
828            total_downloads: i64,
829        }
830
831        let total_downloads = cratesio_crate::Entity::find()
832            .select_only()
833            .column(cratesio_crate::Column::TotalDownloads)
834            .into_model::<Model>()
835            .all(&self.db_con)
836            .await?;
837
838        Ok(total_downloads
839            .iter()
840            .map(|m| m.total_downloads as u64)
841            .sum())
842    }
843
844    async fn authenticate_user(&self, name: &str, pwd: &str) -> DbResult<User> {
845        let user = self.get_user(name).await?;
846
847        match verify_pwd(pwd, &user.pwd, &user.salt).await {
848            PwdVerification::Valid => Ok(user),
849            PwdVerification::ValidLegacy => {
850                // Upgrade the legacy SHA-256 hash now that the plaintext is
851                // known. A failure must not block an otherwise valid login,
852                // the upgrade is retried on the next one.
853                if let Err(e) = self.upgrade_legacy_pwd_hash(&user, pwd).await {
854                    warn!("Failed to upgrade password hash of user {}: {e}", user.name);
855                }
856                Ok(user)
857            }
858            PwdVerification::Invalid => Err(DbError::PasswordMismatch),
859        }
860    }
861
862    async fn increase_download_counter(
863        &self,
864        crate_name: &NormalizedName,
865        crate_version: &Version,
866    ) -> DbResult<()> {
867        let txn = self.db_con.begin().await?;
868
869        krate::Entity::update_many()
870            .col_expr(
871                krate::Column::TotalDownloads,
872                Expr::col(krate::Column::TotalDownloads).add(1),
873            )
874            .filter(krate::Column::Name.eq(crate_name))
875            .exec(&txn)
876            .await?;
877
878        let crate_id = krate::Entity::find()
879            .filter(krate::Column::Name.eq(crate_name))
880            .one(&txn)
881            .await?
882            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))?
883            .id;
884
885        crate_meta::Entity::update_many()
886            .col_expr(
887                crate_meta::Column::Downloads,
888                Expr::col(crate_meta::Column::Downloads).add(1),
889            )
890            .filter(
891                Cond::all()
892                    .add(crate_meta::Column::Version.eq(crate_version))
893                    .add(crate_meta::Column::CrateFk.eq(crate_id)),
894            )
895            .exec(&txn)
896            .await?;
897
898        txn.commit().await?;
899        Ok(())
900    }
901
902    async fn increase_cached_download_counter(
903        &self,
904        crate_name: &NormalizedName,
905        crate_version: &Version,
906    ) -> DbResult<()> {
907        let txn = self.db_con.begin().await?;
908
909        cratesio_crate::Entity::update_many()
910            .col_expr(
911                cratesio_crate::Column::TotalDownloads,
912                Expr::col(cratesio_crate::Column::TotalDownloads).add(1),
913            )
914            .filter(cratesio_crate::Column::Name.eq(crate_name))
915            .exec(&txn)
916            .await?;
917
918        let crate_id = cratesio_crate::Entity::find()
919            .filter(cratesio_crate::Column::Name.eq(crate_name))
920            .one(&txn)
921            .await?
922            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))?
923            .id;
924
925        cratesio_meta::Entity::update_many()
926            .col_expr(
927                cratesio_meta::Column::Downloads,
928                Expr::col(cratesio_meta::Column::Downloads).add(1),
929            )
930            .filter(
931                Cond::all()
932                    .add(cratesio_meta::Column::Version.eq(crate_version))
933                    .add(cratesio_meta::Column::CratesIoFk.eq(crate_id)),
934            )
935            .exec(&txn)
936            .await?;
937
938        txn.commit().await?;
939        Ok(())
940    }
941
942    async fn increase_download_counter_by(
943        &self,
944        crate_name: &NormalizedName,
945        crate_version: &Version,
946        count: u64,
947    ) -> DbResult<()> {
948        let txn = self.db_con.begin().await?;
949
950        krate::Entity::update_many()
951            .col_expr(
952                krate::Column::TotalDownloads,
953                Expr::col(krate::Column::TotalDownloads).add(count as i64),
954            )
955            .filter(krate::Column::Name.eq(crate_name))
956            .exec(&txn)
957            .await?;
958
959        let crate_id = krate::Entity::find()
960            .filter(krate::Column::Name.eq(crate_name))
961            .one(&txn)
962            .await?
963            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))?
964            .id;
965
966        crate_meta::Entity::update_many()
967            .col_expr(
968                crate_meta::Column::Downloads,
969                Expr::col(crate_meta::Column::Downloads).add(count as i64),
970            )
971            .filter(
972                Cond::all()
973                    .add(crate_meta::Column::Version.eq(crate_version))
974                    .add(crate_meta::Column::CrateFk.eq(crate_id)),
975            )
976            .exec(&txn)
977            .await?;
978
979        txn.commit().await?;
980        Ok(())
981    }
982
983    async fn increase_cached_download_counter_by(
984        &self,
985        crate_name: &NormalizedName,
986        crate_version: &Version,
987        count: u64,
988    ) -> DbResult<()> {
989        let txn = self.db_con.begin().await?;
990
991        cratesio_crate::Entity::update_many()
992            .col_expr(
993                cratesio_crate::Column::TotalDownloads,
994                Expr::col(cratesio_crate::Column::TotalDownloads).add(count as i64),
995            )
996            .filter(cratesio_crate::Column::Name.eq(crate_name))
997            .exec(&txn)
998            .await?;
999
1000        let crate_id = cratesio_crate::Entity::find()
1001            .filter(cratesio_crate::Column::Name.eq(crate_name))
1002            .one(&txn)
1003            .await?
1004            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))?
1005            .id;
1006
1007        cratesio_meta::Entity::update_many()
1008            .col_expr(
1009                cratesio_meta::Column::Downloads,
1010                Expr::col(cratesio_meta::Column::Downloads).add(count as i64),
1011            )
1012            .filter(
1013                Cond::all()
1014                    .add(cratesio_meta::Column::Version.eq(crate_version))
1015                    .add(cratesio_meta::Column::CratesIoFk.eq(crate_id)),
1016            )
1017            .exec(&txn)
1018            .await?;
1019
1020        txn.commit().await?;
1021        Ok(())
1022    }
1023
1024    async fn get_last_updated_crate(
1025        &self,
1026        visibility: &CrateVisibility,
1027    ) -> DbResult<Option<(OriginalName, Version)>> {
1028        let mut query = krate::Entity::find();
1029        if let Some(condition) = Self::crate_visibility_condition(visibility) {
1030            query = query.filter(condition);
1031        }
1032        let krate = query
1033            .order_by_desc(krate::Column::LastUpdated)
1034            .one(&self.db_con)
1035            .await?;
1036
1037        if let Some(krate) = krate {
1038            // SAFETY: Unchecked is ok, as only valid crate names are inserted into the database
1039            let name = OriginalName::from_unchecked(krate.original_name);
1040            // SAFETY: Unchecked is ok, as only valid versions are inserted into the database
1041            let version = Version::from_unchecked_str(&krate.max_version);
1042            Ok(Some((name, version)))
1043        } else {
1044            Ok(None)
1045        }
1046    }
1047
1048    async fn validate_session(&self, session_token: &str) -> DbResult<SessionInfo> {
1049        // An expired session is treated like a missing one: the age filter
1050        // makes it invisible here so callers get the same SessionNotFound.
1051        // With expiry disabled (no cutoff) the filter is omitted.
1052        let mut query = user::Entity::find()
1053            .join(JoinType::InnerJoin, user::Relation::Session.def())
1054            .filter(session::Column::Token.eq(hash_token(session_token)));
1055        if let Some(cutoff) = self.session_expiry_cutoff() {
1056            query = query.filter(session::Column::Created.gte(cutoff));
1057        }
1058
1059        let u = query
1060            .one(&self.db_con)
1061            .await?
1062            .ok_or(DbError::SessionNotFound)?;
1063
1064        Ok(SessionInfo {
1065            name: u.name,
1066            is_admin: u.is_admin,
1067            is_read_only: u.is_read_only,
1068        })
1069    }
1070
1071    async fn delete_expired_sessions(&self) -> DbResult<u64> {
1072        // Expiry disabled: nothing to remove.
1073        let Some(cutoff) = self.session_expiry_cutoff() else {
1074            return Ok(0);
1075        };
1076        let res = session::Entity::delete_many()
1077            .filter(session::Column::Created.lt(cutoff))
1078            .exec(&self.db_con)
1079            .await?;
1080        Ok(res.rows_affected)
1081    }
1082
1083    async fn add_session_token(&self, name: &str, session_token: &str) -> DbResult<()> {
1084        let user = self.get_user(name).await?;
1085        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
1086
1087        let s = session::ActiveModel {
1088            token: Set(hash_token(session_token)),
1089            created: Set(created),
1090            user_fk: Set(user.id as i64),
1091            ..Default::default()
1092        };
1093
1094        s.insert(&self.db_con).await?;
1095        Ok(())
1096    }
1097
1098    async fn delete_user_sessions(&self, user_name: &str) -> DbResult<u64> {
1099        let user = self.get_user_model(user_name).await?;
1100        let res = session::Entity::delete_many()
1101            .filter(session::Column::UserFk.eq(user.id))
1102            .exec(&self.db_con)
1103            .await?;
1104        Ok(res.rows_affected)
1105    }
1106
1107    async fn delete_other_user_sessions(
1108        &self,
1109        user_name: &str,
1110        keep_session_token: &str,
1111    ) -> DbResult<u64> {
1112        let user = self.get_user_model(user_name).await?;
1113        let res = session::Entity::delete_many()
1114            .filter(session::Column::UserFk.eq(user.id))
1115            .filter(session::Column::Token.ne(hash_token(keep_session_token)))
1116            .exec(&self.db_con)
1117            .await?;
1118        Ok(res.rows_affected)
1119    }
1120
1121    async fn add_crate_user(&self, crate_name: &NormalizedName, user: &str) -> DbResult<()> {
1122        let crate_fk = self.get_krate_model(crate_name).await?.id;
1123        let user_fk = self.get_user_model(user).await?.id;
1124        self.add_crate_user_impl(crate_fk, user_fk).await
1125    }
1126
1127    async fn add_crate_group(&self, crate_name: &NormalizedName, group: &str) -> DbResult<()> {
1128        let crate_fk = self.get_krate_model(crate_name).await?.id;
1129        let group_fk = self.get_group_model(group).await?.id;
1130        self.add_crate_group_impl(crate_fk, group_fk).await
1131    }
1132
1133    async fn add_group_user(&self, group_name: &str, user: &str) -> DbResult<()> {
1134        let group_fk = self.get_group_model(group_name).await?.id;
1135        let user_fk = self.get_user_model(user).await?.id;
1136        self.add_group_user_impl(group_fk, user_fk).await
1137    }
1138
1139    async fn add_owner(&self, crate_name: &NormalizedName, owner: &str) -> DbResult<()> {
1140        let crate_fk = self.get_krate_model(crate_name).await?.id;
1141        let user_fk = self.get_user_model(owner).await?.id;
1142        self.add_owner_impl(crate_fk, user_fk).await
1143    }
1144
1145    async fn is_download_restricted(&self, crate_name: &NormalizedName) -> DbResult<bool> {
1146        Ok(krate::Entity::find()
1147            .filter(krate::Column::Name.eq(crate_name))
1148            .one(&self.db_con)
1149            .await?
1150            .is_some_and(|model| model.restricted_download))
1151    }
1152
1153    async fn change_download_restricted(
1154        &self,
1155        crate_name: &NormalizedName,
1156        restricted: bool,
1157    ) -> DbResult<()> {
1158        let mut krate: krate::ActiveModel = self.get_krate_model(crate_name).await?.into();
1159        krate.restricted_download = Set(restricted);
1160        krate.update(&self.db_con).await?;
1161        Ok(())
1162    }
1163
1164    async fn is_crate_user(&self, crate_name: &NormalizedName, user: &str) -> DbResult<bool> {
1165        Ok(self
1166            .get_crate_user_by_crate_and_user(crate_name, user)
1167            .await?
1168            .is_some())
1169    }
1170
1171    async fn is_crate_group(&self, crate_name: &NormalizedName, group: &str) -> DbResult<bool> {
1172        Ok(self
1173            .get_crate_group_by_name_and_group(crate_name, group)
1174            .await?
1175            .is_some())
1176    }
1177
1178    async fn is_crate_group_user(&self, crate_name: &NormalizedName, user: &str) -> DbResult<bool> {
1179        let user = user::Entity::find()
1180            .join(JoinType::InnerJoin, user::Relation::GroupUser.def())
1181            .join(JoinType::InnerJoin, group_user::Relation::Group.def())
1182            .join(JoinType::InnerJoin, group::Relation::CrateGroup.def())
1183            .join(JoinType::InnerJoin, crate_group::Relation::Krate.def())
1184            .filter(
1185                Cond::all()
1186                    .add(krate::Column::Name.eq(crate_name))
1187                    .add(user::Column::Name.eq(user)),
1188            )
1189            .one(&self.db_con)
1190            .await?;
1191        Ok(user.is_some())
1192    }
1193
1194    async fn is_group_user(&self, group_name: &str, user: &str) -> DbResult<bool> {
1195        Ok(self
1196            .get_group_user_by_group_and_user(group_name, user)
1197            .await?
1198            .is_some())
1199    }
1200
1201    async fn is_owner(&self, crate_name: &NormalizedName, user: &str) -> DbResult<bool> {
1202        let owner = owner::Entity::find()
1203            .join(JoinType::InnerJoin, owner::Relation::Krate.def())
1204            .join(JoinType::InnerJoin, owner::Relation::User.def())
1205            .filter(
1206                Cond::all()
1207                    .add(krate::Column::Name.eq(crate_name))
1208                    .add(user::Column::Name.eq(user)),
1209            )
1210            .one(&self.db_con)
1211            .await?;
1212
1213        Ok(owner.is_some())
1214    }
1215
1216    async fn get_crate_id(&self, crate_name: &NormalizedName) -> DbResult<Option<i64>> {
1217        let id = krate::Entity::find()
1218            .filter(krate::Column::Name.eq(crate_name))
1219            .one(&self.db_con)
1220            .await?
1221            .map(|model| model.id);
1222
1223        Ok(id)
1224    }
1225
1226    async fn get_crate_owners(&self, crate_name: &NormalizedName) -> DbResult<Vec<User>> {
1227        let u = user::Entity::find()
1228            .join(JoinType::InnerJoin, user::Relation::Owner.def())
1229            .join(JoinType::InnerJoin, owner::Relation::Krate.def())
1230            .filter(Expr::col((CrateIden::Table, krate::Column::Name)).eq(crate_name))
1231            .all(&self.db_con)
1232            .await?;
1233
1234        Ok(u.into_iter().map(User::from).collect())
1235    }
1236
1237    async fn get_crate_users(&self, crate_name: &NormalizedName) -> DbResult<Vec<User>> {
1238        let u = user::Entity::find()
1239            .join(JoinType::InnerJoin, user::Relation::CrateUser.def())
1240            .join(JoinType::InnerJoin, crate_user::Relation::Krate.def())
1241            .filter(Expr::col((CrateIden::Table, krate::Column::Name)).eq(crate_name))
1242            .all(&self.db_con)
1243            .await?;
1244
1245        Ok(u.into_iter().map(User::from).collect())
1246    }
1247
1248    async fn get_crate_groups(&self, crate_name: &NormalizedName) -> DbResult<Vec<Group>> {
1249        let u = group::Entity::find()
1250            .join(JoinType::InnerJoin, group::Relation::CrateGroup.def())
1251            .join(JoinType::InnerJoin, crate_group::Relation::Krate.def())
1252            .filter(Expr::col((CrateIden::Table, krate::Column::Name)).eq(crate_name))
1253            .all(&self.db_con)
1254            .await?;
1255
1256        Ok(u.into_iter().map(Group::from).collect())
1257    }
1258
1259    async fn get_group_users(&self, group_name: &str) -> DbResult<Vec<User>> {
1260        let u = user::Entity::find()
1261            .join(JoinType::InnerJoin, user::Relation::GroupUser.def())
1262            .join(JoinType::InnerJoin, group_user::Relation::Group.def())
1263            .filter(Expr::col((GroupIden::Table, group::Column::Name)).eq(group_name))
1264            .all(&self.db_con)
1265            .await?;
1266
1267        Ok(u.into_iter().map(User::from).collect())
1268    }
1269
1270    async fn get_crate_versions(&self, crate_name: &NormalizedName) -> DbResult<Vec<Version>> {
1271        let u = crate_meta::Entity::find()
1272            .join(JoinType::InnerJoin, crate_meta::Relation::Krate.def())
1273            .filter(Expr::col((CrateIden::Table, krate::Column::Name)).eq(crate_name))
1274            .all(&self.db_con)
1275            .await?;
1276
1277        Ok(u.into_iter()
1278            .map(|meta| Version::from_unchecked_str(&meta.version))
1279            .collect())
1280    }
1281
1282    async fn delete_session_token(&self, session_token: &str) -> DbResult<()> {
1283        if let Some(s) = session::Entity::find()
1284            .filter(session::Column::Token.eq(hash_token(session_token)))
1285            .one(&self.db_con)
1286            .await?
1287        {
1288            s.delete(&self.db_con).await?;
1289        }
1290
1291        Ok(())
1292    }
1293
1294    async fn delete_user(&self, user_name: &str) -> DbResult<()> {
1295        self.get_user_model(user_name)
1296            .await?
1297            .delete(&self.db_con)
1298            .await?;
1299        Ok(())
1300    }
1301
1302    async fn delete_group(&self, group_name: &str) -> DbResult<()> {
1303        self.get_group_model(group_name)
1304            .await?
1305            .delete(&self.db_con)
1306            .await?;
1307        Ok(())
1308    }
1309
1310    async fn change_pwd(&self, user_name: &str, new_pwd: &str) -> DbResult<()> {
1311        let hashed = hash_pwd(new_pwd).await;
1312
1313        let mut u: user::ActiveModel = self.get_user_model(user_name).await?.into();
1314        u.pwd = Set(hashed);
1315        // The argon2 PHC string carries its own salt.
1316        u.salt = Set(String::new());
1317
1318        u.update(&self.db_con).await?;
1319        Ok(())
1320    }
1321
1322    async fn change_read_only_state(&self, user_name: &str, state: bool) -> DbResult<()> {
1323        let mut u: user::ActiveModel = self.get_user_model(user_name).await?.into();
1324        u.is_read_only = Set(state);
1325
1326        u.update(&self.db_con).await?;
1327        Ok(())
1328    }
1329
1330    async fn change_admin_state(&self, user_name: &str, state: bool) -> DbResult<()> {
1331        let mut u: user::ActiveModel = self.get_user_model(user_name).await?.into();
1332        u.is_admin = Set(state);
1333
1334        u.update(&self.db_con).await?;
1335        Ok(())
1336    }
1337
1338    async fn crate_version_exists(&self, crate_id: i64, version: &str) -> DbResult<bool> {
1339        let cm = crate_meta::Entity::find()
1340            .filter(
1341                Cond::all()
1342                    .add(crate_meta::Column::CrateFk.eq(crate_id))
1343                    .add(crate_meta::Column::Version.eq(version)),
1344            )
1345            .one(&self.db_con)
1346            .await?;
1347
1348        Ok(cm.is_some())
1349    }
1350
1351    async fn get_max_version_from_id(&self, crate_id: i64) -> DbResult<Version> {
1352        operations::get_max_version_from_id(&self.db_con, crate_id).await
1353    }
1354
1355    async fn get_max_version_from_name(&self, crate_name: &NormalizedName) -> DbResult<Version> {
1356        let k = self.get_krate_model(crate_name).await?;
1357        let v = Version::try_from(&k.max_version)
1358            .map_err(|_| DbError::FailedToGetMaxVersionByName(crate_name.to_string()))?;
1359        Ok(v)
1360    }
1361
1362    async fn update_max_version(&self, crate_id: i64, version: &Version) -> DbResult<()> {
1363        let krate = krate::Entity::find_by_id(crate_id)
1364            .one(&self.db_con)
1365            .await?
1366            .ok_or(DbError::CrateNotFoundWithId(crate_id))?;
1367
1368        let mut k: krate::ActiveModel = krate.into();
1369        k.max_version = Set(version.to_string());
1370        k.update(&self.db_con).await?;
1371
1372        Ok(())
1373    }
1374
1375    async fn add_auth_token(&self, name: &str, token: &str, user: &str) -> DbResult<()> {
1376        let hashed_token = hash_token(token);
1377        let user = self.get_user_model(user).await?;
1378
1379        let at = auth_token::ActiveModel {
1380            name: Set(name.to_owned()),
1381            token: Set(hashed_token),
1382            user_fk: Set(user.id),
1383            created: Set(Some(Utc::now().format(DB_DATE_FORMAT).to_string())),
1384            ..Default::default()
1385        };
1386
1387        at.insert(&self.db_con).await?;
1388
1389        Ok(())
1390    }
1391
1392    async fn get_user_from_token(&self, token: &str) -> DbResult<User> {
1393        let token = hash_token(token);
1394
1395        let (at, u) = auth_token::Entity::find()
1396            .filter(Expr::col((AuthTokenIden::Table, AuthTokenIden::Token)).eq(token))
1397            .find_also_related(user::Entity)
1398            .one(&self.db_con)
1399            .await?
1400            .ok_or(DbError::TokenNotFound)?;
1401        let u = u.ok_or(DbError::TokenNotFound)?;
1402
1403        // Cargo sends the token with every request, so the date is only
1404        // written when it is older than the interval. A failed update must not
1405        // fail the request, the date is informational.
1406        let now = Utc::now();
1407        let stale_before = (now - TOKEN_LAST_USED_INTERVAL)
1408            .format(DB_DATE_FORMAT)
1409            .to_string();
1410        if at
1411            .last_used
1412            .as_ref()
1413            .is_none_or(|used| *used < stale_before)
1414        {
1415            let mut at: auth_token::ActiveModel = at.into();
1416            at.last_used = Set(Some(now.format(DB_DATE_FORMAT).to_string()));
1417            if let Err(e) = at.update(&self.db_con).await {
1418                warn!("Failed to update the last use of an auth token: {e}");
1419            }
1420        }
1421
1422        Ok(User::from(u))
1423    }
1424
1425    async fn get_user(&self, name: &str) -> DbResult<User> {
1426        Ok(User::from(self.get_user_model(name).await?))
1427    }
1428
1429    async fn get_group(&self, name: &str) -> DbResult<Group> {
1430        Ok(Group::from(self.get_group_model(name).await?))
1431    }
1432
1433    async fn get_auth_token_name(&self, token: &str) -> DbResult<Option<String>> {
1434        Ok(auth_token::Entity::find()
1435            .filter(auth_token::Column::Token.eq(hash_token(token)))
1436            .one(&self.db_con)
1437            .await?
1438            .map(|at| at.name))
1439    }
1440
1441    async fn get_auth_tokens(&self, user_name: &str) -> DbResult<Vec<AuthToken>> {
1442        let at: Vec<auth_token::Model> = auth_token::Entity::find()
1443            .join(JoinType::InnerJoin, auth_token::Relation::User.def())
1444            .filter(user::Column::Name.eq(user_name))
1445            .all(&self.db_con)
1446            .await?;
1447
1448        Ok(at.into_iter().map(AuthToken::from).collect())
1449    }
1450
1451    async fn delete_auth_token(&self, id: i32) -> DbResult<()> {
1452        auth_token::Entity::delete_by_id(id as i64)
1453            .exec(&self.db_con)
1454            .await?;
1455        Ok(())
1456    }
1457
1458    async fn delete_owner(&self, crate_name: &str, owner: &str) -> DbResult<()> {
1459        let model = self.get_owner_by_crate_and_user(crate_name, owner).await?;
1460        self.delete_or_not_found(model, DbError::OwnerNotFound(owner.to_string()))
1461            .await
1462    }
1463
1464    async fn delete_crate_user(&self, crate_name: &NormalizedName, user: &str) -> DbResult<()> {
1465        let model = self
1466            .get_crate_user_by_crate_and_user(crate_name, user)
1467            .await?;
1468        self.delete_or_not_found(model, DbError::UserNotFound(user.to_string()))
1469            .await
1470    }
1471
1472    async fn delete_crate_group(&self, crate_name: &NormalizedName, group: &str) -> DbResult<()> {
1473        let model = self
1474            .get_crate_group_by_name_and_group(crate_name, group)
1475            .await?;
1476        self.delete_or_not_found(model, DbError::GroupNotFound(group.to_string()))
1477            .await
1478    }
1479
1480    async fn delete_group_user(&self, group_name: &str, user: &str) -> DbResult<()> {
1481        let model = self
1482            .get_group_user_by_group_and_user(group_name, user)
1483            .await?;
1484        self.delete_or_not_found(model, DbError::UserNotFound(user.to_string()))
1485            .await
1486    }
1487
1488    async fn add_user(
1489        &self,
1490        name: &str,
1491        pwd: &str,
1492        is_admin: bool,
1493        is_read_only: bool,
1494    ) -> DbResult<()> {
1495        let hashed_pwd = hash_pwd(pwd).await;
1496        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
1497
1498        let u = user::ActiveModel {
1499            name: Set(name.to_owned()),
1500            pwd: Set(hashed_pwd),
1501            salt: Set(String::new()),
1502            is_admin: Set(is_admin),
1503            is_read_only: Set(is_read_only),
1504            created: Set(created),
1505            ..Default::default()
1506        };
1507
1508        u.insert(&self.db_con).await?;
1509        Ok(())
1510    }
1511
1512    async fn add_group(&self, name: &str) -> DbResult<()> {
1513        let g = group::ActiveModel {
1514            name: Set(name.to_owned()),
1515            ..Default::default()
1516        };
1517
1518        g.insert(&self.db_con).await?;
1519        Ok(())
1520    }
1521
1522    async fn get_users(&self) -> DbResult<Vec<User>> {
1523        let users = user::Entity::find()
1524            .order_by_asc(user::Column::Name)
1525            .all(&self.db_con)
1526            .await?;
1527
1528        Ok(users.into_iter().map(User::from).collect())
1529    }
1530
1531    async fn get_groups(&self) -> DbResult<Vec<Group>> {
1532        let groups = group::Entity::find()
1533            .order_by_asc(group::Column::Name)
1534            .all(&self.db_con)
1535            .await?;
1536
1537        Ok(groups.into_iter().map(Group::from).collect())
1538    }
1539
1540    async fn get_total_unique_crates(&self) -> DbResult<u64> {
1541        self.count(
1542            CrateIden::Table,
1543            CrateIden::Id,
1544            DbError::FailedToCountCrates,
1545        )
1546        .await
1547    }
1548
1549    async fn get_total_crate_versions(&self) -> DbResult<u64> {
1550        self.count(
1551            CrateMetaIden::Table,
1552            CrateMetaIden::Id,
1553            DbError::FailedToCountCrateVersions,
1554        )
1555        .await
1556    }
1557
1558    async fn get_total_downloads(&self) -> DbResult<u64> {
1559        #[derive(FromQueryResult)]
1560        struct Model {
1561            total_downloads: i64,
1562        }
1563
1564        Ok(krate::Entity::find()
1565            .select_only()
1566            .column(krate::Column::TotalDownloads)
1567            .into_model::<Model>()
1568            .all(&self.db_con)
1569            .await?
1570            .into_iter()
1571            .map(|m| m.total_downloads as u64)
1572            .sum())
1573    }
1574
1575    async fn get_top_crates_downloads(
1576        &self,
1577        top: u64,
1578        visibility: &CrateVisibility,
1579    ) -> DbResult<Vec<(String, u64)>> {
1580        #[derive(Debug, PartialEq, FromQueryResult)]
1581        struct SelectResult {
1582            original_name: String,
1583            total_downloads: i64,
1584        }
1585
1586        let mut query = Query::select()
1587            .columns(vec![CrateIden::OriginalName, CrateIden::TotalDownloads])
1588            .from(CrateIden::Table)
1589            .order_by(CrateIden::TotalDownloads, Order::Desc)
1590            .limit(top)
1591            .to_owned();
1592        if let Some(condition) = Self::crate_visibility_condition(visibility) {
1593            query.cond_where(condition);
1594        }
1595        let stmt = self.db_con.get_database_backend().build(&query);
1596
1597        Ok(SelectResult::find_by_statement(stmt)
1598            .all(&self.db_con)
1599            .await?
1600            .into_iter()
1601            .map(|x| (x.original_name, x.total_downloads as u64))
1602            .collect())
1603    }
1604
1605    async fn get_crate_summaries(&self) -> DbResult<Vec<CrateSummary>> {
1606        let krates = krate::Entity::find()
1607            .order_by(krate::Column::Name, Order::Asc)
1608            .all(&self.db_con)
1609            .await?;
1610
1611        Ok(krates.into_iter().map(CrateSummary::from).collect())
1612    }
1613
1614    async fn add_doc_queue(
1615        &self,
1616        krate: &NormalizedName,
1617        version: &Version,
1618        path: &Path,
1619    ) -> DbResult<()> {
1620        let s = doc_queue::ActiveModel {
1621            krate: Set(krate.to_string()),
1622            version: Set(version.to_string()),
1623            // FIXME: Convert Path to String properly, handle errors
1624            path: Set(path.to_string_lossy().to_string()),
1625            ..Default::default()
1626        };
1627
1628        s.insert(&self.db_con).await?;
1629        Ok(())
1630    }
1631
1632    async fn delete_doc_queue(&self, id: i64) -> DbResult<()> {
1633        DocQueue::delete_by_id(id).exec(&self.db_con).await?;
1634        Ok(())
1635    }
1636
1637    async fn get_doc_queue(&self) -> DbResult<Vec<DocQueueEntry>> {
1638        let entities = DocQueue::find().all(&self.db_con).await?;
1639
1640        Ok(entities.into_iter().map(DocQueueEntry::from).collect())
1641    }
1642
1643    async fn delete_crate(&self, krate: &NormalizedName, version: &Version) -> DbResult<()> {
1644        let txn = self.db_con.begin().await?;
1645
1646        // Delete the entry from the "crate_meta" table
1647        let crate_meta_version = crate_meta::Entity::find()
1648            .join(JoinType::InnerJoin, crate_meta::Relation::Krate.def())
1649            .filter(krate::Column::Name.eq(krate))
1650            .filter(crate_meta::Column::Version.eq(version))
1651            .one(&txn)
1652            .await?
1653            .ok_or_else(|| DbError::CrateMetaNotFound(krate.to_string(), version.to_string()))?;
1654        let crate_id = crate_meta_version.crate_fk;
1655        let current_max_version = operations::get_max_version_from_id(&txn, crate_id).await?;
1656        crate_meta_version.delete(&txn).await?;
1657
1658        // Delete the crate index entry from "crate_index" table
1659        let crate_index_version = crate_index::Entity::find()
1660            .join(JoinType::InnerJoin, crate_index::Relation::Krate.def())
1661            .filter(krate::Column::Name.eq(krate))
1662            .filter(crate_index::Column::Vers.eq(version))
1663            .one(&txn)
1664            .await?
1665            .ok_or_else(|| DbError::CrateIndexNotFound(krate.to_string(), version.to_string()))?;
1666        crate_index_version.delete(&txn).await?;
1667
1668        // If it was the last entry in the "crate_meta" table, delete the entry
1669        // in the "crate" table as well
1670        let crate_meta_rows = crate_meta::Entity::find()
1671            .join(JoinType::InnerJoin, crate_meta::Relation::Krate.def())
1672            .filter(krate::Column::Name.eq(krate))
1673            .all(&txn)
1674            .await?;
1675
1676        if crate_meta_rows.is_empty() {
1677            krate::Entity::delete_many()
1678                .filter(krate::Column::Name.eq(krate))
1679                .exec(&txn)
1680                .await?;
1681        } else {
1682            let c = krate::Entity::find_by_id(crate_id)
1683                .one(&txn)
1684                .await?
1685                .ok_or(DbError::CrateNotFoundWithId(crate_id))?;
1686            let mut c: krate::ActiveModel = c.into();
1687
1688            // Update the max. version if the deleted version was the max. version.
1689            if version == &current_max_version {
1690                let new_max_version = crate_meta_rows
1691                    .into_iter()
1692                    .map(|cm| Version::from_unchecked_str(&cm.version))
1693                    .max()
1694                    .unwrap(); // Safe to unwrap, as crate_meta_rows is not empty
1695                c.max_version = Set(new_max_version.into_inner());
1696            }
1697            // Update the ETag value of the crate index.
1698            let etag = operations::compute_etag(&txn, krate, crate_id).await?;
1699            c.e_tag = Set(etag);
1700            c.update(&txn).await?;
1701        }
1702
1703        txn.commit().await?;
1704
1705        Ok(())
1706    }
1707
1708    async fn get_crate_meta_list(&self, crate_name: &NormalizedName) -> DbResult<Vec<CrateMeta>> {
1709        let crate_meta = crate_meta::Entity::find()
1710            .join(JoinType::InnerJoin, crate_meta::Relation::Krate.def())
1711            .filter(krate::Column::Name.eq(crate_name))
1712            .all(&self.db_con)
1713            .await?;
1714
1715        let crate_meta = crate_meta
1716            .into_iter()
1717            .map(|cm| CrateMeta {
1718                name: crate_name.to_string(),
1719                id: cm.id,
1720                version: cm.version,
1721                created: cm.created,
1722                downloads: cm.downloads,
1723                crate_fk: cm.crate_fk,
1724            })
1725            .collect();
1726
1727        Ok(crate_meta)
1728    }
1729
1730    async fn update_last_updated(&self, id: i64, last_updated: &DateTime<Utc>) -> DbResult<()> {
1731        let krate = krate::Entity::find_by_id(id)
1732            .one(&self.db_con)
1733            .await?
1734            .ok_or(DbError::CrateNotFoundWithId(id))?;
1735
1736        let date = last_updated.format(DB_DATE_FORMAT).to_string();
1737
1738        let mut krate: krate::ActiveModel = krate.into();
1739        krate.last_updated = Set(date);
1740        krate.update(&self.db_con).await?;
1741
1742        Ok(())
1743    }
1744
1745    async fn search_in_crate_name_and_description(
1746        &self,
1747        contains: &str,
1748        limit: u64,
1749        offset: u64,
1750        cache: bool,
1751        visibility: &CrateVisibility,
1752    ) -> DbResult<Vec<CrateOverview>> {
1753        self.query_crates(
1754            Some(contains),
1755            Some((limit, offset)),
1756            cache,
1757            visibility,
1758            CrateSort::Name,
1759            &CrateTagFilter::default(),
1760        )
1761        .await
1762    }
1763
1764    async fn count_by_crate_name_and_description(
1765        &self,
1766        contains: &str,
1767        cache: bool,
1768        visibility: &CrateVisibility,
1769    ) -> DbResult<u64> {
1770        self.count_crates(
1771            Some(contains),
1772            cache,
1773            visibility,
1774            &CrateTagFilter::default(),
1775        )
1776        .await
1777    }
1778
1779    async fn get_crate_overview_list(
1780        &self,
1781        limit: u64,
1782        offset: u64,
1783        cache: bool,
1784        visibility: &CrateVisibility,
1785        sort: CrateSort,
1786        tags: &CrateTagFilter,
1787    ) -> DbResult<Vec<CrateOverview>> {
1788        self.query_crates(None, Some((limit, offset)), cache, visibility, sort, tags)
1789            .await
1790    }
1791
1792    async fn count_crate_overview_list(
1793        &self,
1794        cache: bool,
1795        visibility: &CrateVisibility,
1796        tags: &CrateTagFilter,
1797    ) -> DbResult<u64> {
1798        self.count_crates(None, cache, visibility, tags).await
1799    }
1800
1801    async fn get_crate_data(&self, crate_name: &NormalizedName) -> DbResult<CrateData> {
1802        let krate = self.get_krate_model(crate_name).await?;
1803
1804        let owners: Vec<String> = krate
1805            .find_related(owner::Entity)
1806            .find_also_related(user::Entity)
1807            .all(&self.db_con)
1808            .await?
1809            .into_iter()
1810            .filter_map(|(_, v)| v.map(|v| v.name))
1811            .collect();
1812        let categories: Vec<String> = krate
1813            .find_related(crate_category_to_crate::Entity)
1814            .find_also_related(crate_category::Entity)
1815            .all(&self.db_con)
1816            .await?
1817            .into_iter()
1818            .filter_map(|(_, v)| v.map(|v| v.category))
1819            .collect();
1820        let keywords: Vec<String> = krate
1821            .find_related(crate_keyword_to_crate::Entity)
1822            .find_also_related(crate_keyword::Entity)
1823            .all(&self.db_con)
1824            .await?
1825            .into_iter()
1826            .filter_map(|(_, v)| v.map(|v| v.keyword))
1827            .collect();
1828        let authors: Vec<String> = krate
1829            .find_related(crate_author_to_crate::Entity)
1830            .find_also_related(crate_author::Entity)
1831            .all(&self.db_con)
1832            .await?
1833            .into_iter()
1834            .filter_map(|(_, v)| v.map(|v| v.author))
1835            .collect();
1836        let crate_metas = krate
1837            .find_related(crate_meta::Entity)
1838            .all(&self.db_con)
1839            .await?;
1840        let crate_indices = krate
1841            .find_related(crate_index::Entity)
1842            .all(&self.db_con)
1843            .await?;
1844
1845        // The dependencies of all versions, looked up together
1846        let mut index_deps: HashMap<&str, Vec<IndexDep>> = HashMap::new();
1847        for ci in &crate_indices {
1848            if let Some(deps) = ci.deps.clone() {
1849                let deps = serde_json::from_value::<Vec<IndexDep>>(deps)
1850                    .map_err(|e| DbError::FailedToConvertToJson(e.to_string()))?;
1851                index_deps.insert(ci.vers.as_str(), deps);
1852            }
1853        }
1854        let dep_info =
1855            operations::get_dep_info(&self.db_con, index_deps.values().flatten()).await?;
1856
1857        let mut versions = Vec::new();
1858        for cm in crate_metas {
1859            let ci = crate_indices
1860                .iter()
1861                .find(|ci| ci.vers == cm.version)
1862                .ok_or_else(|| {
1863                    DbError::CrateIndexNotFound(krate.name.clone(), cm.version.clone())
1864                })?;
1865            let dependencies: Vec<CrateRegistryDep> = index_deps
1866                .get(ci.vers.as_str())
1867                .into_iter()
1868                .flatten()
1869                .map(|dep| {
1870                    let info = dep_info
1871                        .get(&operations::dep_lookup_name(dep))
1872                        .cloned()
1873                        .unwrap_or_default();
1874                    CrateRegistryDep::from_index(info.description, info.latest_version, dep.clone())
1875                })
1876                .collect();
1877            let features: BTreeMap<String, Vec<String>> = match ci.features.clone() {
1878                Some(features) => serde_json::from_value::<BTreeMap<String, Vec<String>>>(features)
1879                    .map_err(|e| DbError::FailedToConvertToJson(e.to_string()))?,
1880                None => BTreeMap::default(),
1881            };
1882
1883            versions.push(CrateVersionData {
1884                version: cm.version,
1885                created: cm.created,
1886                downloads: cm.downloads,
1887                readme: cm.readme,
1888                license: cm.license,
1889                license_file: cm.license_file,
1890                documentation: cm.documentation,
1891                dependencies,
1892                checksum: ci.cksum.clone(),
1893                features,
1894                yanked: ci.yanked,
1895                links: ci.links.clone(),
1896                v: ci.v,
1897                size: cm.size,
1898                rust_version: ci.rust_version.clone(),
1899                publisher: cm.publisher.clone(),
1900                publisher_token: cm.publisher_token.clone(),
1901            });
1902        }
1903        versions.sort_by(|a, b| {
1904            Version::from_unchecked_str(&b.version).cmp(&Version::from_unchecked_str(&a.version))
1905        });
1906
1907        let crate_data = CrateData {
1908            name: krate.original_name,
1909            owners,
1910            max_version: krate.max_version,
1911            total_downloads: krate.total_downloads,
1912            last_updated: krate.last_updated,
1913            homepage: krate.homepage,
1914            description: krate.description,
1915            repository: krate.repository,
1916            categories,
1917            keywords,
1918            authors,
1919            versions,
1920        };
1921
1922        Ok(crate_data)
1923    }
1924
1925    async fn get_crate_dependents(&self, crate_name: &NormalizedName) -> DbResult<CrateDependents> {
1926        let krate = self.get_krate_model(crate_name).await?;
1927
1928        // Requirements on the crate, by the non-yanked version that has them
1929        let requirements: Vec<(i64, String, String, String)> = crate_dependency::Entity::find()
1930            .select_only()
1931            .column(crate_index::Column::CrateFk)
1932            .column(crate_index::Column::Vers)
1933            .column(crate_dependency::Column::VersionReq)
1934            .column(crate_dependency::Column::Kind)
1935            .inner_join(crate_index::Entity)
1936            .filter(crate_dependency::Column::Name.eq(crate_name))
1937            .filter(crate_index::Column::Yanked.eq(false))
1938            .filter(crate_index::Column::CrateFk.ne(krate.id))
1939            .into_tuple()
1940            .all(&self.db_con)
1941            .await?;
1942        let mut requirements_by_version: HashMap<(i64, String), Vec<(String, String)>> =
1943            HashMap::new();
1944        for (crate_fk, vers, version_req, kind) in requirements {
1945            requirements_by_version
1946                .entry((crate_fk, vers))
1947                .or_default()
1948                .push((version_req, kind));
1949        }
1950
1951        // Only the latest version of a dependent counts, so the versions of
1952        // the crate and of every crate that ever depended on it are needed
1953        let mut crate_ids: Vec<i64> = requirements_by_version
1954            .keys()
1955            .map(|(crate_fk, _)| *crate_fk)
1956            .collect();
1957        crate_ids.push(krate.id);
1958        crate_ids.sort_unstable();
1959        crate_ids.dedup();
1960        let mut versions_by_crate: HashMap<i64, (String, Vec<String>)> = HashMap::new();
1961        // Chunked to stay below the bind parameter limit of SQLite
1962        for ids in crate_ids.chunks(1000) {
1963            let versions: Vec<(i64, String, String)> = crate_index::Entity::find()
1964                .select_only()
1965                .column(crate_index::Column::CrateFk)
1966                .column(krate::Column::OriginalName)
1967                .column(crate_index::Column::Vers)
1968                .inner_join(krate::Entity)
1969                .filter(crate_index::Column::CrateFk.is_in(ids.iter().copied()))
1970                .filter(crate_index::Column::Yanked.eq(false))
1971                .into_tuple()
1972                .all(&self.db_con)
1973                .await?;
1974            for (crate_fk, name, vers) in versions {
1975                versions_by_crate
1976                    .entry(crate_fk)
1977                    .or_insert_with(|| (name, Vec::new()))
1978                    .1
1979                    .push(vers);
1980            }
1981        }
1982
1983        let latest = versions_by_crate
1984            .remove(&krate.id)
1985            .and_then(|(_, versions)| latest_version(versions.iter().map(String::as_str)));
1986        let mut dependents: Vec<CrateDependent> = versions_by_crate
1987            .into_iter()
1988            .filter_map(|(crate_fk, (name, versions))| {
1989                let version = latest_version(versions.iter().map(String::as_str))?;
1990                let requirements = requirements_by_version.remove(&(crate_fk, version.clone()))?;
1991                Some(CrateDependent::new(
1992                    name,
1993                    version,
1994                    requirements,
1995                    latest.as_deref(),
1996                ))
1997            })
1998            .collect();
1999        dependents.sort_by(|a, b| a.name.cmp(&b.name));
2000
2001        Ok(CrateDependents {
2002            latest_version: latest,
2003            dependents,
2004            restricted: 0,
2005        })
2006    }
2007
2008    async fn add_empty_crate(&self, name: &str, created: &DateTime<Utc>) -> DbResult<i64> {
2009        let created = created.format(DB_DATE_FORMAT).to_string();
2010        let normalized_name = NormalizedName::from(
2011            OriginalName::try_from(name)
2012                .map_err(|_| DbError::InvalidCrateName(name.to_string()))?,
2013        );
2014        operations::ensure_no_similar_crate(&self.db_con, &normalized_name).await?;
2015        let krate = krate::ActiveModel {
2016            id: ActiveValue::default(),
2017            name: Set(normalized_name.to_string()),
2018            original_name: Set(name.to_string()),
2019            max_version: Set("0.0.0".to_string()),
2020            last_updated: Set(created),
2021            total_downloads: Set(0),
2022            homepage: Set(None),
2023            description: Set(None),
2024            repository: Set(None),
2025            e_tag: Set(String::new()), // Set to empty string, as it can be computed, when the crate index is inserted
2026            restricted_download: Set(false),
2027        };
2028        Ok(krate.insert(&self.db_con).await?.id)
2029    }
2030
2031    async fn add_crate(
2032        &self,
2033        pub_metadata: &PublishMetadata,
2034        cksum: &str,
2035        size: i64,
2036        created: &DateTime<Utc>,
2037        owner: &str,
2038        token_name: Option<String>,
2039    ) -> DbResult<i64> {
2040        let created = created.format(DB_DATE_FORMAT).to_string();
2041        let normalized_name = NormalizedName::from(
2042            OriginalName::try_from(&pub_metadata.name)
2043                .map_err(|_| DbError::InvalidCrateName(pub_metadata.name.clone()))?,
2044        );
2045
2046        // Look up the crate inside the transaction and by its normalized name, so
2047        // a concurrent first publish of the same crate cannot slip in between.
2048        let txn = self.db_con.begin().await?;
2049        let existing = krate::Entity::find()
2050            .filter(krate::Column::Name.eq(&normalized_name))
2051            .one(&txn)
2052            .await?;
2053
2054        let crate_id = if let Some(krate) = existing {
2055            let krate_id = krate.id;
2056            operations::ensure_owner_or_admin(&txn, owner, &normalized_name, krate_id).await?;
2057            let max_version = max(
2058                parse_db_version(&krate.max_version)?,
2059                parse_db_version(&pub_metadata.vers)?,
2060            );
2061
2062            let mut krate: krate::ActiveModel = krate.into();
2063            krate.last_updated = Set(created.clone());
2064            krate.max_version = Set(max_version.into_inner());
2065            krate.homepage = Set(pub_metadata.homepage.clone());
2066            krate.description = Set(pub_metadata.description.clone());
2067            krate.repository = Set(pub_metadata.repository.clone());
2068            krate.e_tag = Set(String::new()); // Set to empty string, as it can be computed, when the crate index is inserted
2069            krate.update(&txn).await?;
2070            krate_id
2071        } else {
2072            operations::ensure_no_similar_crate(&txn, &normalized_name).await?;
2073            let krate = krate::ActiveModel {
2074                id: ActiveValue::default(),
2075                name: Set(normalized_name.to_string()),
2076                original_name: Set(pub_metadata.name.clone()),
2077                max_version: Set(pub_metadata.vers.clone()),
2078                last_updated: Set(created.clone()),
2079                total_downloads: Set(0),
2080                homepage: Set(pub_metadata.homepage.clone()),
2081                description: Set(pub_metadata.description.clone()),
2082                repository: Set(pub_metadata.repository.clone()),
2083                e_tag: Set(String::new()), // Set to empty string, as it can be computed, when the crate index is inserted
2084                restricted_download: Set(false),
2085            };
2086            let krate = krate.insert(&txn).await?;
2087            krate.id
2088        };
2089
2090        operations::add_owner_if_not_exists(&txn, owner, crate_id).await?;
2091        operations::add_crate_metadata(
2092            &txn,
2093            pub_metadata,
2094            size,
2095            &created,
2096            crate_id,
2097            Some(owner),
2098            token_name.as_deref(),
2099        )
2100        .await?;
2101        operations::add_crate_index(&txn, pub_metadata, cksum, crate_id).await?;
2102        operations::update_etag(&txn, &pub_metadata.name, crate_id).await?;
2103        operations::update_crate_categories(&txn, pub_metadata, crate_id).await?;
2104        operations::update_crate_keywords(&txn, pub_metadata, crate_id).await?;
2105        operations::update_crate_authors(&txn, pub_metadata, crate_id).await?;
2106
2107        txn.commit().await?;
2108        Ok(crate_id)
2109    }
2110
2111    async fn update_docs_link(
2112        &self,
2113        crate_name: &NormalizedName,
2114        version: &Version,
2115        docs_link: &str,
2116    ) -> DbResult<()> {
2117        let (cm, _c) = crate_meta::Entity::find()
2118            .find_also_related(krate::Entity)
2119            .filter(
2120                Cond::all()
2121                    .add(krate::Column::Name.eq(crate_name))
2122                    .add(crate_meta::Column::Version.eq(version)),
2123            )
2124            .one(&self.db_con)
2125            .await?
2126            .ok_or_else(|| DbError::CrateNotFound(crate_name.to_string()))?;
2127
2128        let mut cm: crate_meta::ActiveModel = cm.into();
2129        cm.documentation = Set(Some(docs_link.to_string()));
2130        cm.update(&self.db_con).await?;
2131        Ok(())
2132    }
2133
2134    async fn add_crate_metadata(
2135        &self,
2136        pub_metadata: &PublishMetadata,
2137        size: i64,
2138        created: &str,
2139        crate_id: i64,
2140    ) -> DbResult<()> {
2141        operations::add_crate_metadata(
2142            &self.db_con,
2143            pub_metadata,
2144            size,
2145            created,
2146            crate_id,
2147            None,
2148            None,
2149        )
2150        .await
2151    }
2152
2153    async fn get_prefetch_data(&self, crate_name: &str) -> DbResult<Prefetch> {
2154        let mut krate = krate::Entity::find()
2155            .filter(krate::Column::Name.eq(crate_name))
2156            .find_with_related(crate_index::Entity)
2157            .all(&self.db_con)
2158            .await?
2159            .into_iter();
2160
2161        // Expecting only one crate with its related indices
2162        let (Some((krate, crate_indices)), None) = (krate.next(), krate.next()) else {
2163            return Err(DbError::CrateNotFound(crate_name.to_string()));
2164        };
2165
2166        let index_metadata =
2167            operations::crate_index_model_to_index_metadata(crate_name, crate_indices)?;
2168        let data = operations::index_metadata_to_bytes(&index_metadata)?;
2169
2170        Ok(Prefetch {
2171            data,
2172            etag: krate.e_tag,
2173            last_modified: krate.last_updated,
2174        })
2175    }
2176
2177    async fn is_cratesio_cache_up_to_date(
2178        &self,
2179        crate_name: &NormalizedName,
2180        etag: Option<String>,
2181        last_modified: Option<String>,
2182    ) -> DbResult<PrefetchState> {
2183        let Some(krate) = cratesio_crate::Entity::find()
2184            .filter(cratesio_crate::Column::Name.eq(crate_name))
2185            .one(&self.db_con)
2186            .await?
2187        else {
2188            return Ok(PrefetchState::NotFound);
2189        };
2190
2191        let needs_update = match (etag, last_modified) {
2192            (Some(etag), Some(last_modified)) => {
2193                krate.e_tag != etag || krate.last_modified != last_modified
2194            }
2195            (Some(etag), None) => krate.e_tag != etag,
2196            (None, Some(last_modified)) => krate.last_modified != last_modified,
2197            (None, None) => true,
2198        };
2199
2200        if !needs_update {
2201            Ok(PrefetchState::UpToDate {
2202                etag: krate.e_tag,
2203                last_modified: krate.last_modified,
2204            })
2205        } else {
2206            let crate_indices = krate
2207                .find_related(cratesio_index::Entity)
2208                .all(&self.db_con)
2209                .await?;
2210            let index_metadata =
2211                operations::cratesio_index_model_to_index_metadata(crate_name, crate_indices)?;
2212            let data = operations::index_metadata_to_bytes(&index_metadata)?;
2213
2214            Ok(PrefetchState::NeedsUpdate(Prefetch {
2215                data,
2216                etag: krate.e_tag.clone(),
2217                last_modified: krate.last_modified,
2218            }))
2219        }
2220    }
2221
2222    async fn add_cratesio_prefetch_data(
2223        &self,
2224        crate_name: &OriginalName,
2225        etag: &str,
2226        last_modified: &str,
2227        description: Option<String>,
2228        indices: &[IndexMetadata],
2229        licenses: &HashMap<String, String>,
2230    ) -> DbResult<Prefetch> {
2231        let normalized_name = crate_name.to_normalized();
2232
2233        let max_version = indices
2234            .iter()
2235            .map(|i| Version::from_unchecked_str(&i.vers))
2236            .max()
2237            .ok_or_else(|| DbError::FailedToGetMaxVersionByName(crate_name.to_string()))?;
2238
2239        // Use a transaction to ensure the etag update and all index inserts are
2240        // atomic. Without this, a partial failure would leave the crate record
2241        // with the latest etag but incomplete index data, causing stale cache
2242        // entries that can never self-heal (the background updater would get 304
2243        // from crates.io because the etag matches).
2244        let txn = self.db_con.begin().await?;
2245
2246        let krate = cratesio_crate::Entity::find()
2247            .filter(cratesio_crate::Column::Name.eq(&normalized_name))
2248            .one(&txn)
2249            .await?;
2250
2251        let krate = if let Some(krate) = krate {
2252            let mut krate: cratesio_crate::ActiveModel = krate.into();
2253            krate.e_tag = Set(etag.to_string());
2254            krate.last_modified = Set(last_modified.to_string());
2255            krate.max_version = Set(max_version.into_inner());
2256            krate.update(&txn).await?
2257        } else {
2258            let krate = cratesio_crate::ActiveModel {
2259                id: ActiveValue::default(),
2260                name: Set(normalized_name.to_string()),
2261                original_name: Set(crate_name.to_string()),
2262                description: Set(description),
2263                e_tag: Set(etag.to_string()),
2264                last_modified: Set(last_modified.to_string()),
2265                total_downloads: Set(0),
2266                max_version: Set(max_version.to_string()),
2267            };
2268            krate.insert(&txn).await?
2269        };
2270
2271        let current_indices = cratesio_index::Entity::find()
2272            .filter(cratesio_index::Column::CratesIoFk.eq(krate.id))
2273            .all(&txn)
2274            .await?;
2275
2276        for index in indices {
2277            // Refresh the mutable parts of an already cached version. "yanked"
2278            // can flip upstream at any time. "rust_version" is refreshed too
2279            // because caches written before the column existed hold NULL for
2280            // every row, and only a refresh can heal them.
2281            if let Some(current_index) = current_indices.iter().find(|ci| index.vers == ci.vers) {
2282                let mut ci: cratesio_index::ActiveModel = current_index.to_owned().into();
2283                let mut changed = false;
2284
2285                if index.yanked != current_index.yanked {
2286                    ci.yanked = Set(index.yanked);
2287                    changed = true;
2288                }
2289
2290                if index.rust_version != current_index.rust_version {
2291                    ci.rust_version = Set(index.rust_version.clone());
2292                    changed = true;
2293                }
2294
2295                if changed {
2296                    ci.update(&txn).await?;
2297                }
2298            } else {
2299                let deps = if index.deps.is_empty() {
2300                    None
2301                } else {
2302                    let deps = serde_json::to_value(&index.deps)
2303                        .map_err(|e| DbError::FailedToConvertToJson(e.to_string()))?;
2304                    Some(deps)
2305                };
2306
2307                let features = serde_json::to_value(&index.features)
2308                    .map_err(|e| DbError::FailedToConvertToJson(e.to_string()))?;
2309
2310                let features2 = serde_json::to_value(&index.features2)
2311                    .map_err(|e| DbError::FailedToConvertToJson(e.to_string()))?;
2312
2313                let new_index = cratesio_index::ActiveModel {
2314                    id: ActiveValue::default(),
2315                    name: Set(index.name.clone()),
2316                    vers: Set(index.vers.clone()),
2317                    deps: Set(deps),
2318                    cksum: Set(index.cksum.clone()),
2319                    features: Set(Some(features)),
2320                    features2: Set(Some(features2)),
2321                    yanked: Set(index.yanked),
2322                    links: Set(index.links.clone()),
2323                    pubtime: Set(index.pubtime.map(|dt| dt.naive_utc())),
2324                    v: Set(index.v.unwrap_or(1) as i32),
2325                    crates_io_fk: Set(krate.id),
2326                    rust_version: Set(index.rust_version.clone()),
2327                };
2328
2329                new_index.insert(&txn).await?;
2330
2331                // Add the meta data for the crate version.
2332                let meta = cratesio_meta::ActiveModel {
2333                    id: ActiveValue::default(),
2334                    version: Set(index.vers.clone()),
2335                    downloads: Set(0),
2336                    crates_io_fk: Set(krate.id),
2337                    documentation: Set(Some(format!(
2338                        "https://docs.rs/{normalized_name}/{}",
2339                        index.vers,
2340                    ))),
2341                    license: Set(licenses.get(&index.vers).cloned()),
2342                };
2343
2344                meta.insert(&txn).await?;
2345            }
2346        }
2347
2348        // The license of a published version cannot change upstream, so only
2349        // versions cached before it was fetched successfully get it here.
2350        let unlicensed = cratesio_meta::Entity::find()
2351            .filter(cratesio_meta::Column::CratesIoFk.eq(krate.id))
2352            .filter(cratesio_meta::Column::License.is_null())
2353            .all(&txn)
2354            .await?;
2355        for meta in unlicensed {
2356            if let Some(license) = licenses.get(&meta.version) {
2357                let mut meta: cratesio_meta::ActiveModel = meta.into();
2358                meta.license = Set(Some(license.clone()));
2359                meta.update(&txn).await?;
2360            }
2361        }
2362
2363        txn.commit().await?;
2364
2365        Ok(Prefetch {
2366            data: operations::index_metadata_to_bytes(indices)?,
2367            etag: etag.to_string(),
2368            last_modified: last_modified.to_string(),
2369        })
2370    }
2371
2372    async fn get_cratesio_index_update_list(&self) -> DbResult<Vec<CratesioPrefetchMsg>> {
2373        let crates = cratesio_crate::Entity::find().all(&self.db_con).await?;
2374        let msgs = crates
2375            .into_iter()
2376            .map(|krate| {
2377                CratesioPrefetchMsg::Update(UpdateData {
2378                    name: OriginalName::from_unchecked(krate.original_name),
2379                    etag: Some(krate.e_tag),
2380                    last_modified: Some(krate.last_modified),
2381                })
2382            })
2383            .collect();
2384        Ok(msgs)
2385    }
2386
2387    async fn get_cratesio_version_meta(
2388        &self,
2389        crate_name: &NormalizedName,
2390        version: &Version,
2391    ) -> DbResult<Option<CratesioVersionMeta>> {
2392        #[derive(FromQueryResult)]
2393        struct Meta {
2394            cksum: String,
2395            license: Option<String>,
2396        }
2397
2398        // One query, as it runs for every download from the crates.io proxy
2399        let version = version.to_string();
2400        let meta_version = version.clone();
2401        let meta_of_version =
2402            cratesio_crate::Relation::CratesioMeta
2403                .def()
2404                .on_condition(move |_crate, meta| {
2405                    Condition::all().add(
2406                        Expr::col((meta, cratesio_meta::Column::Version)).eq(meta_version.clone()),
2407                    )
2408                });
2409        let meta = cratesio_index::Entity::find()
2410            .select_only()
2411            .column(cratesio_index::Column::Cksum)
2412            .column(cratesio_meta::Column::License)
2413            .inner_join(cratesio_crate::Entity)
2414            .join(JoinType::LeftJoin, meta_of_version)
2415            .filter(cratesio_crate::Column::Name.eq(crate_name))
2416            .filter(cratesio_index::Column::Vers.eq(version))
2417            .into_model::<Meta>()
2418            .one(&self.db_con)
2419            .await?;
2420
2421        Ok(meta.map(|m| CratesioVersionMeta {
2422            cksum: m.cksum,
2423            license: m.license,
2424        }))
2425    }
2426
2427    async fn get_crate_license(
2428        &self,
2429        crate_name: &NormalizedName,
2430        version: &Version,
2431    ) -> DbResult<Option<String>> {
2432        let meta = crate_meta::Entity::find()
2433            .inner_join(krate::Entity)
2434            .filter(krate::Column::Name.eq(crate_name))
2435            .filter(crate_meta::Column::Version.eq(version))
2436            .one(&self.db_con)
2437            .await?;
2438        Ok(meta.and_then(|m| m.license))
2439    }
2440
2441    async fn unyank_crate(&self, crate_name: &NormalizedName, version: &Version) -> DbResult<()> {
2442        let mut ci: crate_index::ActiveModel = self
2443            .get_crate_index_model(crate_name, version)
2444            .await?
2445            .into();
2446        ci.yanked = Set(false);
2447        ci.save(&self.db_con).await?;
2448        Ok(())
2449    }
2450
2451    async fn yank_crate(&self, crate_name: &NormalizedName, version: &Version) -> DbResult<()> {
2452        let mut ci: crate_index::ActiveModel = self
2453            .get_crate_index_model(crate_name, version)
2454            .await?
2455            .into();
2456        ci.yanked = Set(true);
2457        ci.save(&self.db_con).await?;
2458        Ok(())
2459    }
2460
2461    async fn register_webhook(&self, webhook: Webhook) -> DbResult<String> {
2462        let w = webhook::ActiveModel {
2463            event: Set(Into::<&str>::into(webhook.event).to_string()),
2464            callback_url: Set(webhook.callback_url),
2465            name: Set(webhook.name),
2466            secret: Set(webhook.secret),
2467            ..Default::default()
2468        };
2469
2470        let w: webhook::Model = w.insert(&self.db_con).await?;
2471        Ok(w.id.to_string())
2472    }
2473
2474    async fn delete_webhook(&self, id: &str) -> DbResult<()> {
2475        self.get_webhook_model(id)
2476            .await?
2477            .delete(&self.db_con)
2478            .await?;
2479        Ok(())
2480    }
2481
2482    async fn get_webhook(&self, id: &str) -> DbResult<Webhook> {
2483        webhook_model_to_obj(self.get_webhook_model(id).await?)
2484    }
2485
2486    async fn get_all_webhooks(&self) -> DbResult<Vec<Webhook>> {
2487        Ok(webhook::Entity::find()
2488            .all(&self.db_con)
2489            .await?
2490            .into_iter()
2491            .filter_map(|w| webhook_model_to_obj(w).ok())
2492            .collect())
2493    }
2494
2495    async fn add_webhook_queue(
2496        &self,
2497        event: WebhookEvent,
2498        payload: serde_json::Value,
2499    ) -> DbResult<()> {
2500        let w = webhook::Entity::find()
2501            .filter(webhook::Column::Event.eq(Into::<&str>::into(event)))
2502            .all(&self.db_con)
2503            .await?;
2504
2505        if w.is_empty() {
2506            return Ok(());
2507        }
2508
2509        let now = Utc::now();
2510
2511        let entries = w.into_iter().map(|w| webhook_queue::ActiveModel {
2512            webhook_fk: Set(w.id),
2513            payload: Set(payload.clone()),
2514            next_attempt: Set(now.into()),
2515            last_attempt: Set(None),
2516            ..Default::default()
2517        });
2518
2519        webhook_queue::Entity::insert_many(entries)
2520            .exec(&self.db_con)
2521            .await?;
2522        Ok(())
2523    }
2524    async fn get_pending_webhook_queue_entries(
2525        &self,
2526        timestamp: DateTime<Utc>,
2527    ) -> DbResult<Vec<WebhookQueue>> {
2528        let w = webhook_queue::Entity::find()
2529            .find_with_related(webhook::Entity)
2530            .filter(webhook_queue::Column::NextAttempt.lte(timestamp))
2531            .all(&self.db_con)
2532            .await?;
2533
2534        Ok(w.into_iter()
2535            .filter_map(|w| {
2536                let webhook = w.1.first()?;
2537                Some(WebhookQueue {
2538                    id: w.0.id.to_string(),
2539                    callback_url: webhook.callback_url.clone(),
2540                    payload: w.0.payload,
2541                    last_attempt: w.0.last_attempt.map(Into::into),
2542                    next_attempt: w.0.next_attempt.into(),
2543                    secret: webhook.secret.clone(),
2544                })
2545            })
2546            .collect())
2547    }
2548
2549    async fn update_webhook_queue(
2550        &self,
2551        id: &str,
2552        last_attempt: DateTime<Utc>,
2553        next_attempt: DateTime<Utc>,
2554    ) -> DbResult<()> {
2555        let mut w: webhook_queue::ActiveModel = self.get_webhook_queue_model(id).await?.into();
2556        w.last_attempt = Set(Some(last_attempt.into()));
2557        w.next_attempt = Set(next_attempt.into());
2558        w.update(&self.db_con).await?;
2559        Ok(())
2560    }
2561
2562    async fn delete_webhook_queue(&self, id: &str) -> DbResult<()> {
2563        self.get_webhook_queue_model(id)
2564            .await?
2565            .delete(&self.db_con)
2566            .await?;
2567        Ok(())
2568    }
2569
2570    // OAuth2 identity methods
2571
2572    async fn get_user_by_oauth2_identity(
2573        &self,
2574        issuer: &str,
2575        subject: &str,
2576    ) -> DbResult<Option<User>> {
2577        let identity = oauth2_identity::Entity::find()
2578            .filter(oauth2_identity::Column::ProviderIssuer.eq(issuer))
2579            .filter(oauth2_identity::Column::Subject.eq(subject))
2580            .one(&self.db_con)
2581            .await?;
2582
2583        if let Some(identity) = identity {
2584            let u = user::Entity::find_by_id(identity.user_fk)
2585                .one(&self.db_con)
2586                .await?
2587                .ok_or_else(|| DbError::UserNotFound(format!("user_id={}", identity.user_fk)))?;
2588
2589            Ok(Some(User::from(u)))
2590        } else {
2591            Ok(None)
2592        }
2593    }
2594
2595    async fn has_oauth2_admin(&self) -> DbResult<bool> {
2596        let found = user::Entity::find()
2597            .filter(user::Column::IsAdmin.eq(true))
2598            .inner_join(oauth2_identity::Entity)
2599            .one(&self.db_con)
2600            .await?;
2601
2602        Ok(found.is_some())
2603    }
2604
2605    async fn create_oauth2_user(
2606        &self,
2607        username: &str,
2608        issuer: &str,
2609        subject: &str,
2610        email: Option<String>,
2611        is_admin: bool,
2612        is_read_only: bool,
2613    ) -> DbResult<User> {
2614        // OAuth2 users do not use password auth, but the column needs a
2615        // valid hash. Hash before opening the transaction to keep it short.
2616        let random_pwd = Uuid::new_v4().to_string();
2617        let hashed_pwd = hash_pwd(&random_pwd).await;
2618        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
2619
2620        // Use a transaction to ensure atomicity
2621        let txn = self.db_con.begin().await?;
2622
2623        // Create the user
2624        let new_user = user::ActiveModel {
2625            name: Set(username.to_string()),
2626            pwd: Set(hashed_pwd),
2627            salt: Set(String::new()),
2628            is_admin: Set(is_admin),
2629            is_read_only: Set(is_read_only),
2630            created: Set(created.clone()),
2631            ..Default::default()
2632        };
2633
2634        let res = user::Entity::insert(new_user).exec(&txn).await?;
2635        let user_id = res.last_insert_id;
2636
2637        // Link the OAuth2 identity
2638        let identity = oauth2_identity::ActiveModel {
2639            user_fk: Set(user_id),
2640            provider_issuer: Set(issuer.to_string()),
2641            subject: Set(subject.to_string()),
2642            email: Set(email),
2643            created: Set(created.clone()),
2644            ..Default::default()
2645        };
2646
2647        oauth2_identity::Entity::insert(identity).exec(&txn).await?;
2648
2649        txn.commit().await?;
2650
2651        Ok(User {
2652            id: user_id as i32,
2653            name: username.to_string(),
2654            pwd: String::new(),  // Don't expose password hash
2655            salt: String::new(), // Don't expose salt
2656            is_admin,
2657            is_read_only,
2658            created,
2659        })
2660    }
2661
2662    async fn link_oauth2_identity(
2663        &self,
2664        user_id: i64,
2665        issuer: &str,
2666        subject: &str,
2667        email: Option<String>,
2668    ) -> DbResult<()> {
2669        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
2670        let identity = oauth2_identity::ActiveModel {
2671            user_fk: Set(user_id),
2672            provider_issuer: Set(issuer.to_string()),
2673            subject: Set(subject.to_string()),
2674            email: Set(email),
2675            created: Set(created),
2676            ..Default::default()
2677        };
2678
2679        oauth2_identity::Entity::insert(identity)
2680            .exec(&self.db_con)
2681            .await?;
2682
2683        Ok(())
2684    }
2685
2686    // OAuth2 state methods (CSRF/PKCE during auth flow)
2687
2688    async fn store_oauth2_state(
2689        &self,
2690        state: &str,
2691        pkce_verifier: &str,
2692        nonce: &str,
2693    ) -> DbResult<()> {
2694        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
2695        let s = oauth2_state::ActiveModel {
2696            state: Set(state.to_string()),
2697            pkce_verifier: Set(pkce_verifier.to_string()),
2698            nonce: Set(nonce.to_string()),
2699            created: Set(created),
2700            ..Default::default()
2701        };
2702
2703        oauth2_state::Entity::insert(s).exec(&self.db_con).await?;
2704        Ok(())
2705    }
2706
2707    async fn get_and_delete_oauth2_state(&self, state: &str) -> DbResult<Option<OAuth2StateData>> {
2708        let Some(s) = oauth2_state::Entity::find()
2709            .filter(oauth2_state::Column::State.eq(state))
2710            .one(&self.db_con)
2711            .await?
2712        else {
2713            return Ok(None);
2714        };
2715
2716        // The state is single use. Only the request whose delete actually
2717        // removed the row may continue, so two concurrent callbacks with the
2718        // same state cannot both succeed.
2719        let deleted = oauth2_state::Entity::delete_many()
2720            .filter(oauth2_state::Column::Id.eq(s.id))
2721            .exec(&self.db_con)
2722            .await?
2723            .rows_affected;
2724        if deleted == 0 || s.created < oauth2_state_expiry_cutoff() {
2725            return Ok(None);
2726        }
2727
2728        Ok(Some(OAuth2StateData {
2729            state: s.state,
2730            pkce_verifier: s.pkce_verifier,
2731            nonce: s.nonce,
2732        }))
2733    }
2734
2735    async fn cleanup_expired_oauth2_states(&self) -> DbResult<u64> {
2736        let result = oauth2_state::Entity::delete_many()
2737            .filter(oauth2_state::Column::Created.lt(oauth2_state_expiry_cutoff()))
2738            .exec(&self.db_con)
2739            .await?;
2740
2741        Ok(result.rows_affected)
2742    }
2743
2744    async fn is_username_available(&self, username: &str) -> DbResult<bool> {
2745        let existing = user::Entity::find()
2746            .filter(user::Column::Name.eq(username))
2747            .one(&self.db_con)
2748            .await?;
2749
2750        Ok(existing.is_none())
2751    }
2752
2753    // Toolchain distribution methods
2754
2755    async fn add_toolchain(
2756        &self,
2757        name: &str,
2758        version: &str,
2759        date: &str,
2760        channel: Option<String>,
2761    ) -> DbResult<i64> {
2762        let created = Utc::now().format(DB_DATE_FORMAT).to_string();
2763
2764        let model = toolchain::ActiveModel {
2765            id: ActiveValue::NotSet,
2766            name: Set(name.to_string()),
2767            version: Set(version.to_string()),
2768            date: Set(date.to_string()),
2769            channel: Set(channel),
2770            created: Set(created),
2771        };
2772
2773        let result = model.insert(&self.db_con).await?;
2774        Ok(result.id)
2775    }
2776
2777    async fn add_toolchain_target(
2778        &self,
2779        toolchain_id: i64,
2780        target: &str,
2781        storage_path: &str,
2782        hash: &str,
2783        size: i64,
2784    ) -> DbResult<i64> {
2785        let model = toolchain_target::ActiveModel {
2786            id: ActiveValue::NotSet,
2787            toolchain_fk: Set(toolchain_id),
2788            target: Set(target.to_string()),
2789            storage_path: Set(storage_path.to_string()),
2790            hash: Set(hash.to_string()),
2791            size: Set(size),
2792            status: Set("processing".to_string()),
2793        };
2794
2795        let result = model.insert(&self.db_con).await?;
2796        Ok(result.id)
2797    }
2798
2799    async fn set_target_status(&self, target_id: i64, status: &str) -> DbResult<()> {
2800        toolchain_target::Entity::update_many()
2801            .col_expr(toolchain_target::Column::Status, Expr::value(status))
2802            .filter(toolchain_target::Column::Id.eq(target_id))
2803            .exec(&self.db_con)
2804            .await?;
2805        Ok(())
2806    }
2807
2808    async fn add_toolchain_component(
2809        &self,
2810        target_id: i64,
2811        name: &str,
2812        storage_path: &str,
2813        hash: &str,
2814        size: i64,
2815    ) -> DbResult<()> {
2816        let model = toolchain_component::ActiveModel {
2817            id: ActiveValue::NotSet,
2818            toolchain_target_fk: Set(target_id),
2819            name: Set(name.to_string()),
2820            storage_path: Set(storage_path.to_string()),
2821            hash: Set(hash.to_string()),
2822            size: Set(size),
2823        };
2824
2825        model.insert(&self.db_con).await?;
2826        Ok(())
2827    }
2828
2829    async fn get_toolchain_by_channel(
2830        &self,
2831        channel: &str,
2832    ) -> DbResult<Option<ToolchainWithTargets>> {
2833        let tc = toolchain::Entity::find()
2834            .filter(toolchain::Column::Channel.eq(channel))
2835            .one(&self.db_con)
2836            .await?;
2837
2838        Ok(match tc {
2839            Some(tc) => Some(self.toolchain_with_targets(tc).await?),
2840            None => None,
2841        })
2842    }
2843
2844    async fn get_toolchain_by_version(
2845        &self,
2846        name: &str,
2847        version: &str,
2848    ) -> DbResult<Option<ToolchainWithTargets>> {
2849        Ok(
2850            match self.get_toolchain_by_name_version(name, version).await? {
2851                Some(tc) => Some(self.toolchain_with_targets(tc).await?),
2852                None => None,
2853            },
2854        )
2855    }
2856
2857    async fn list_toolchains(&self) -> DbResult<Vec<ToolchainWithTargets>> {
2858        let toolchains = toolchain::Entity::find()
2859            .order_by_desc(toolchain::Column::Created)
2860            .all(&self.db_con)
2861            .await?;
2862
2863        let mut result = Vec::with_capacity(toolchains.len());
2864
2865        for tc in toolchains {
2866            result.push(self.toolchain_with_targets(tc).await?);
2867        }
2868
2869        Ok(result)
2870    }
2871
2872    async fn delete_toolchain(&self, name: &str, version: &str) -> DbResult<()> {
2873        toolchain::Entity::delete_many()
2874            .filter(toolchain::Column::Name.eq(name))
2875            .filter(toolchain::Column::Version.eq(version))
2876            .exec(&self.db_con)
2877            .await?;
2878
2879        Ok(())
2880    }
2881
2882    async fn delete_toolchain_target(
2883        &self,
2884        name: &str,
2885        version: &str,
2886        target: &str,
2887    ) -> DbResult<()> {
2888        if let Some(tc) = self.get_toolchain_by_name_version(name, version).await? {
2889            toolchain_target::Entity::delete_many()
2890                .filter(toolchain_target::Column::ToolchainFk.eq(tc.id))
2891                .filter(toolchain_target::Column::Target.eq(target))
2892                .exec(&self.db_con)
2893                .await?;
2894        }
2895
2896        Ok(())
2897    }
2898
2899    async fn set_channel(&self, channel: &str, name: &str, version: &str) -> DbResult<()> {
2900        // First, clear the channel from any other toolchain that has it
2901        let toolchains_with_channel = toolchain::Entity::find()
2902            .filter(toolchain::Column::Channel.eq(channel))
2903            .all(&self.db_con)
2904            .await?;
2905
2906        for tc in toolchains_with_channel {
2907            let mut model: toolchain::ActiveModel = tc.into();
2908            model.channel = Set(None);
2909            model.update(&self.db_con).await?;
2910        }
2911
2912        // Then set the channel on the target toolchain
2913        if let Some(tc) = self.get_toolchain_by_name_version(name, version).await? {
2914            let mut model: toolchain::ActiveModel = tc.into();
2915            model.channel = Set(Some(channel.to_string()));
2916            model.update(&self.db_con).await?;
2917        }
2918
2919        Ok(())
2920    }
2921
2922    async fn get_channels(&self) -> DbResult<Vec<ChannelInfo>> {
2923        let toolchains = toolchain::Entity::find()
2924            .filter(toolchain::Column::Channel.is_not_null())
2925            .all(&self.db_con)
2926            .await?;
2927
2928        Ok(toolchains
2929            .into_iter()
2930            .filter_map(|tc| {
2931                tc.channel.map(|channel| ChannelInfo {
2932                    name: channel,
2933                    version: tc.version,
2934                    date: tc.date,
2935                })
2936            })
2937            .collect())
2938    }
2939}
2940
2941fn parse_db_version(value: &str) -> DbResult<Version> {
2942    Version::try_from(value).map_err(|_| DbError::InvalidVersion(value.to_owned()))
2943}
2944
2945#[cfg(test)]
2946mod like_escape_tests {
2947    use super::escape_like_pattern;
2948
2949    #[test]
2950    fn escapes_wildcards_and_backslash() {
2951        assert_eq!(escape_like_pattern("a%b_c"), "a\\%b\\_c");
2952        assert_eq!(escape_like_pattern("100%"), "100\\%");
2953        // Backslash is escaped first so it doesn't double-escape the wildcards.
2954        assert_eq!(escape_like_pattern("a\\b"), "a\\\\b");
2955    }
2956
2957    #[test]
2958    fn leaves_plain_text_untouched() {
2959        assert_eq!(escape_like_pattern("serde-json"), "serde-json");
2960    }
2961}
2962
2963#[cfg(test)]
2964mod session_age_tests {
2965    use super::{MAX_SESSION_AGE, session_age_to_duration};
2966
2967    #[test]
2968    fn zero_means_never_expire() {
2969        assert_eq!(session_age_to_duration(std::time::Duration::ZERO), None);
2970    }
2971
2972    #[test]
2973    fn positive_age_is_preserved() {
2974        assert_eq!(
2975            session_age_to_duration(std::time::Duration::from_hours(1)),
2976            Some(chrono::Duration::seconds(3600))
2977        );
2978    }
2979
2980    #[test]
2981    fn absurd_age_is_clamped_without_panicking() {
2982        // u64::MAX seconds would overflow chrono::Duration if not clamped.
2983        let resolved = session_age_to_duration(std::time::Duration::from_secs(u64::MAX))
2984            .expect("non-zero age yields Some");
2985        assert_eq!(
2986            resolved,
2987            chrono::Duration::from_std(MAX_SESSION_AGE).unwrap()
2988        );
2989    }
2990}