1use crate::DbPool;
2use chrono::Utc;
3use rand::Rng;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Project {
8 pub id: i64,
9 pub name: String,
10 pub slug: String,
11 pub api_key: String,
12 pub created_at: String,
13}
14
15fn generate_api_key() -> String {
17 let mut rng = rand::thread_rng();
18 let bytes: [u8; 24] = rng.r#gen();
19 format!("proj_{}", hex::encode(bytes))
20}
21
22fn slugify(name: &str) -> String {
24 name.to_lowercase()
25 .chars()
26 .map(|c| if c.is_alphanumeric() { c } else { '-' })
27 .collect::<String>()
28 .split('-')
29 .filter(|s| !s.is_empty())
30 .collect::<Vec<_>>()
31 .join("-")
32}
33
34pub fn ensure_default_project(pool: &DbPool) -> anyhow::Result<Project> {
36 let conn = pool.get()?;
37
38 let count: i64 = conn.query_row("SELECT COUNT(*) FROM projects", [], |row| row.get(0))?;
40
41 if count == 0 {
42 let now = Utc::now().to_rfc3339();
43 let api_key = generate_api_key();
44
45 conn.execute(
46 "INSERT INTO projects (name, slug, api_key, created_at) VALUES (?1, ?2, ?3, ?4)",
47 ("Default", "default", &api_key, &now),
48 )?;
49
50 tracing::info!("Created default project with API key: {}", api_key);
51
52 return Ok(Project {
53 id: conn.last_insert_rowid(),
54 name: "Default".to_string(),
55 slug: "default".to_string(),
56 api_key,
57 created_at: now,
58 });
59 }
60
61 let project = conn.query_row(
63 "SELECT id, name, slug, api_key, created_at FROM projects ORDER BY id LIMIT 1",
64 [],
65 |row| {
66 Ok(Project {
67 id: row.get(0)?,
68 name: row.get(1)?,
69 slug: row.get(2)?,
70 api_key: row.get(3)?,
71 created_at: row.get(4)?,
72 })
73 },
74 )?;
75
76 Ok(project)
77}
78
79pub fn list_all(pool: &DbPool) -> anyhow::Result<Vec<Project>> {
81 let conn = pool.get()?;
82 let mut stmt = conn.prepare(
83 "SELECT id, name, slug, api_key, strftime('%Y-%m-%d %H:%M', created_at) FROM projects ORDER BY name",
84 )?;
85
86 let projects = stmt
87 .query_map([], |row| {
88 Ok(Project {
89 id: row.get(0)?,
90 name: row.get(1)?,
91 slug: row.get(2)?,
92 api_key: row.get(3)?,
93 created_at: row.get(4)?,
94 })
95 })?
96 .collect::<Result<Vec<_>, _>>()?;
97
98 Ok(projects)
99}
100
101pub fn find(pool: &DbPool, id: i64) -> anyhow::Result<Option<Project>> {
103 let conn = pool.get()?;
104
105 let project = conn
106 .query_row(
107 "SELECT id, name, slug, api_key, created_at FROM projects WHERE id = ?1",
108 [id],
109 |row| {
110 Ok(Project {
111 id: row.get(0)?,
112 name: row.get(1)?,
113 slug: row.get(2)?,
114 api_key: row.get(3)?,
115 created_at: row.get(4)?,
116 })
117 },
118 )
119 .ok();
120
121 Ok(project)
122}
123
124pub fn find_by_slug(pool: &DbPool, slug: &str) -> anyhow::Result<Option<Project>> {
126 let conn = pool.get()?;
127
128 let project = conn
129 .query_row(
130 "SELECT id, name, slug, api_key, created_at FROM projects WHERE slug = ?1",
131 [slug],
132 |row| {
133 Ok(Project {
134 id: row.get(0)?,
135 name: row.get(1)?,
136 slug: row.get(2)?,
137 api_key: row.get(3)?,
138 created_at: row.get(4)?,
139 })
140 },
141 )
142 .ok();
143
144 Ok(project)
145}
146
147pub fn find_by_api_key(pool: &DbPool, api_key: &str) -> anyhow::Result<Option<Project>> {
149 let conn = pool.get()?;
150
151 let project = conn
152 .query_row(
153 "SELECT id, name, slug, api_key, created_at FROM projects WHERE api_key = ?1",
154 [api_key],
155 |row| {
156 Ok(Project {
157 id: row.get(0)?,
158 name: row.get(1)?,
159 slug: row.get(2)?,
160 api_key: row.get(3)?,
161 created_at: row.get(4)?,
162 })
163 },
164 )
165 .ok();
166
167 Ok(project)
168}
169
170pub fn create(pool: &DbPool, name: &str) -> anyhow::Result<Project> {
172 let conn = pool.get()?;
173
174 let now = Utc::now().to_rfc3339();
175 let slug = slugify(name);
176 let api_key = generate_api_key();
177
178 conn.execute(
179 "INSERT INTO projects (name, slug, api_key, created_at) VALUES (?1, ?2, ?3, ?4)",
180 (name, &slug, &api_key, &now),
181 )?;
182
183 let project_id = conn.last_insert_rowid();
184
185 Ok(Project {
186 id: project_id,
187 name: name.to_string(),
188 slug,
189 api_key,
190 created_at: now,
191 })
192}
193
194pub fn delete(pool: &DbPool, id: i64) -> anyhow::Result<()> {
196 let conn = pool.get()?;
197 conn.execute("DELETE FROM projects WHERE id = ?1", [id])?;
198 Ok(())
199}
200
201pub fn regenerate_api_key(pool: &DbPool, id: i64) -> anyhow::Result<String> {
203 let conn = pool.get()?;
204 let new_key = generate_api_key();
205
206 conn.execute(
207 "UPDATE projects SET api_key = ?1 WHERE id = ?2",
208 (&new_key, id),
209 )?;
210
211 Ok(new_key)
212}
213
214pub fn count(pool: &DbPool) -> anyhow::Result<i64> {
216 let conn = pool.get()?;
217 let count: i64 = conn.query_row("SELECT COUNT(*) FROM projects", [], |row| row.get(0))?;
218 Ok(count)
219}