Skip to main content

max / makenotwork

4.1 KB · 168 lines History Blame Raw
1 use sqlx::PgPool;
2
3 use crate::db::models::*;
4 use crate::db::{ItemId, ProjectId, SyncAppId, UserId};
5 use crate::error::Result;
6
7 // ── Sync Apps ──
8
9 /// Compute the SHA-256 hash of an API key (hex-encoded).
10 pub fn hash_api_key(api_key: &str) -> String {
11 use sha2::Digest;
12 let hash = sha2::Sha256::digest(api_key.as_bytes());
13 hex::encode(hash)
14 }
15
16 /// Create a new sync app. Stores the hashed API key and prefix.
17 #[tracing::instrument(skip_all)]
18 pub async fn create_sync_app(
19 pool: &PgPool,
20 creator_id: UserId,
21 name: &str,
22 api_key: &str,
23 project_id: Option<ProjectId>,
24 item_id: Option<ItemId>,
25 ) -> Result<DbSyncApp> {
26 let key_hash = hash_api_key(api_key);
27 let key_prefix = &api_key[..8.min(api_key.len())];
28 let app = sqlx::query_as::<_, DbSyncApp>(
29 r#"
30 INSERT INTO sync_apps (creator_id, name, api_key_hash, api_key_prefix, project_id, item_id)
31 VALUES ($1, $2, $3, $4, $5, $6)
32 RETURNING *
33 "#,
34 )
35 .bind(creator_id)
36 .bind(name)
37 .bind(&key_hash)
38 .bind(key_prefix)
39 .bind(project_id)
40 .bind(item_id)
41 .fetch_one(pool)
42 .await?;
43
44 Ok(app)
45 }
46
47 /// Update the project/item link for a sync app.
48 #[tracing::instrument(skip_all)]
49 pub async fn update_sync_app_link(
50 pool: &PgPool,
51 app_id: SyncAppId,
52 project_id: Option<ProjectId>,
53 item_id: Option<ItemId>,
54 ) -> Result<DbSyncApp> {
55 let app = sqlx::query_as::<_, DbSyncApp>(
56 r#"
57 UPDATE sync_apps SET project_id = $2, item_id = $3
58 WHERE id = $1
59 RETURNING *
60 "#,
61 )
62 .bind(app_id)
63 .bind(project_id)
64 .bind(item_id)
65 .fetch_one(pool)
66 .await?;
67
68 Ok(app)
69 }
70
71 /// Get a sync app by API key (only if active). Hashes the input before lookup.
72 #[tracing::instrument(skip_all)]
73 pub async fn get_sync_app_by_api_key(
74 pool: &PgPool,
75 api_key: &str,
76 ) -> Result<Option<DbSyncApp>> {
77 let key_hash = hash_api_key(api_key);
78 let app = sqlx::query_as::<_, DbSyncApp>(
79 "SELECT * FROM sync_apps WHERE api_key_hash = $1 AND is_active = true",
80 )
81 .bind(&key_hash)
82 .fetch_optional(pool)
83 .await?;
84
85 Ok(app)
86 }
87
88 /// Get a sync app by ID.
89 #[tracing::instrument(skip_all)]
90 pub async fn get_sync_app_by_id(pool: &PgPool, id: SyncAppId) -> Result<Option<DbSyncApp>> {
91 let app = sqlx::query_as::<_, DbSyncApp>(
92 "SELECT * FROM sync_apps WHERE id = $1",
93 )
94 .bind(id)
95 .fetch_optional(pool)
96 .await?;
97
98 Ok(app)
99 }
100
101 /// List all sync apps for a creator.
102 #[tracing::instrument(skip_all)]
103 pub async fn get_sync_apps_by_creator(
104 pool: &PgPool,
105 creator_id: UserId,
106 ) -> Result<Vec<DbSyncApp>> {
107 let apps = sqlx::query_as::<_, DbSyncApp>(
108 "SELECT * FROM sync_apps WHERE creator_id = $1 ORDER BY created_at DESC LIMIT 100",
109 )
110 .bind(creator_id)
111 .fetch_all(pool)
112 .await?;
113
114 Ok(apps)
115 }
116
117 /// Get all sync apps linked to a specific project.
118 pub async fn get_sync_apps_by_project(
119 pool: &PgPool,
120 project_id: ProjectId,
121 ) -> Result<Vec<DbSyncApp>> {
122 let apps = sqlx::query_as::<_, DbSyncApp>(
123 "SELECT * FROM sync_apps WHERE project_id = $1 ORDER BY created_at DESC LIMIT 100",
124 )
125 .bind(project_id)
126 .fetch_all(pool)
127 .await?;
128
129 Ok(apps)
130 }
131
132 /// Regenerate an API key for a sync app. Stores the hashed key and prefix.
133 #[tracing::instrument(skip_all)]
134 pub async fn regenerate_sync_app_key(
135 pool: &PgPool,
136 app_id: SyncAppId,
137 new_api_key: &str,
138 ) -> Result<DbSyncApp> {
139 let key_hash = hash_api_key(new_api_key);
140 let key_prefix = &new_api_key[..8.min(new_api_key.len())];
141 let app = sqlx::query_as::<_, DbSyncApp>(
142 r#"
143 UPDATE sync_apps SET api_key_hash = $2, api_key_prefix = $3
144 WHERE id = $1
145 RETURNING *
146 "#,
147 )
148 .bind(app_id)
149 .bind(&key_hash)
150 .bind(key_prefix)
151 .fetch_one(pool)
152 .await?;
153
154 Ok(app)
155 }
156
157 /// Delete a sync app (cascades to devices and log entries).
158 #[tracing::instrument(skip_all)]
159 pub async fn delete_sync_app(pool: &PgPool, app_id: SyncAppId) -> Result<()> {
160 sqlx::query("DELETE FROM sync_apps WHERE id = $1")
161 .bind(app_id)
162 .execute(pool)
163 .await?;
164
165 Ok(())
166 }
167
168