| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | use std::collections::HashMap; |
| 9 | |
| 10 | use sqlx::Row; |
| 11 | |
| 12 | use crate::{Store, StoreError, new_id, now_ms}; |
| 13 | |
| 14 | |
| 15 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] |
| 16 | pub struct AdminCounts { |
| 17 | |
| 18 | pub users: i64, |
| 19 | |
| 20 | pub disabled_users: i64, |
| 21 | |
| 22 | pub admins: i64, |
| 23 | |
| 24 | pub repos: i64, |
| 25 | |
| 26 | pub groups: i64, |
| 27 | |
| 28 | pub issues: i64, |
| 29 | |
| 30 | pub open_issues: i64, |
| 31 | |
| 32 | pub pulls: i64, |
| 33 | |
| 34 | pub keys: i64, |
| 35 | |
| 36 | pub tokens: i64, |
| 37 | |
| 38 | pub sessions: i64, |
| 39 | } |
| 40 | |
| 41 | |
| 42 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 43 | pub struct SignupInvite { |
| 44 | |
| 45 | pub id: String, |
| 46 | |
| 47 | pub note: Option<String>, |
| 48 | |
| 49 | pub expires_at: Option<i64>, |
| 50 | |
| 51 | pub used_at: Option<i64>, |
| 52 | |
| 53 | pub used_by: Option<String>, |
| 54 | |
| 55 | pub created_at: i64, |
| 56 | } |
| 57 | |
| 58 | |
| 59 | |
| 60 | #[derive(Debug, Clone)] |
| 61 | pub struct NewSignupInvite { |
| 62 | |
| 63 | pub token_hash: String, |
| 64 | |
| 65 | pub note: Option<String>, |
| 66 | |
| 67 | pub expires_at: Option<i64>, |
| 68 | |
| 69 | pub created_by: String, |
| 70 | } |
| 71 | |
| 72 | impl Store { |
| 73 | |
| 74 | |
| 75 | |
| 76 | |
| 77 | |
| 78 | pub async fn admin_counts(&self) -> Result<AdminCounts, StoreError> { |
| 79 | let count = |sql: &'static str| async move { |
| 80 | let n: i64 = sqlx::query(sql).fetch_one(&self.pool).await?.try_get("n")?; |
| 81 | Ok::<i64, StoreError>(n) |
| 82 | }; |
| 83 | |
| 84 | |
| 85 | let count_b = |sql: &'static str, b: bool| async move { |
| 86 | let n: i64 = sqlx::query(sql) |
| 87 | .bind(b) |
| 88 | .fetch_one(&self.pool) |
| 89 | .await? |
| 90 | .try_get("n")?; |
| 91 | Ok::<i64, StoreError>(n) |
| 92 | }; |
| 93 | Ok(AdminCounts { |
| 94 | users: count("SELECT COUNT(*) AS n FROM users").await?, |
| 95 | disabled_users: count("SELECT COUNT(*) AS n FROM users WHERE disabled_at IS NOT NULL") |
| 96 | .await?, |
| 97 | admins: count_b("SELECT COUNT(*) AS n FROM users WHERE is_admin = $1", true).await?, |
| 98 | repos: count("SELECT COUNT(*) AS n FROM repos").await?, |
| 99 | groups: count("SELECT COUNT(*) AS n FROM groups").await?, |
| 100 | issues: count_b("SELECT COUNT(*) AS n FROM issues WHERE is_pull = $1", false).await?, |
| 101 | open_issues: count_b( |
| 102 | "SELECT COUNT(*) AS n FROM issues WHERE state = 'open' AND is_pull = $1", |
| 103 | false, |
| 104 | ) |
| 105 | .await?, |
| 106 | pulls: count_b("SELECT COUNT(*) AS n FROM issues WHERE is_pull = $1", true).await?, |
| 107 | keys: count("SELECT COUNT(*) AS n FROM keys").await?, |
| 108 | tokens: count("SELECT COUNT(*) AS n FROM api_tokens WHERE revoked_at IS NULL").await?, |
| 109 | sessions: count("SELECT COUNT(*) AS n FROM sessions").await?, |
| 110 | }) |
| 111 | } |
| 112 | |
| 113 | |
| 114 | |
| 115 | |
| 116 | |
| 117 | |
| 118 | pub async fn total_repo_bytes(&self) -> Result<i64, StoreError> { |
| 119 | let n: i64 = sqlx::query("SELECT COALESCE(SUM(size_bytes), 0) AS n FROM repos") |
| 120 | .fetch_one(&self.pool) |
| 121 | .await? |
| 122 | .try_get("n")?; |
| 123 | Ok(n) |
| 124 | } |
| 125 | |
| 126 | |
| 127 | |
| 128 | |
| 129 | |
| 130 | |
| 131 | |
| 132 | |
| 133 | pub async fn create_signup_invite( |
| 134 | &self, |
| 135 | new: NewSignupInvite, |
| 136 | ) -> Result<SignupInvite, StoreError> { |
| 137 | let invite = SignupInvite { |
| 138 | id: new_id(), |
| 139 | note: new.note, |
| 140 | expires_at: new.expires_at, |
| 141 | used_at: None, |
| 142 | used_by: None, |
| 143 | created_at: now_ms(), |
| 144 | }; |
| 145 | sqlx::query( |
| 146 | "INSERT INTO signup_invites \ |
| 147 | (id, token_hash, note, expires_at, used_at, used_by, created_by, created_at) \ |
| 148 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", |
| 149 | ) |
| 150 | .bind(&invite.id) |
| 151 | .bind(&new.token_hash) |
| 152 | .bind(&invite.note) |
| 153 | .bind(invite.expires_at) |
| 154 | .bind(invite.used_at) |
| 155 | .bind(&invite.used_by) |
| 156 | .bind(&new.created_by) |
| 157 | .bind(invite.created_at) |
| 158 | .execute(&self.pool) |
| 159 | .await?; |
| 160 | Ok(invite) |
| 161 | } |
| 162 | |
| 163 | |
| 164 | |
| 165 | |
| 166 | |
| 167 | |
| 168 | pub async fn list_signup_invites(&self) -> Result<Vec<SignupInvite>, StoreError> { |
| 169 | let rows = sqlx::query( |
| 170 | "SELECT id, note, expires_at, used_at, used_by, created_at \ |
| 171 | FROM signup_invites ORDER BY created_at DESC", |
| 172 | ) |
| 173 | .fetch_all(&self.pool) |
| 174 | .await?; |
| 175 | rows.iter() |
| 176 | .map(|r| { |
| 177 | Ok(SignupInvite { |
| 178 | id: r.try_get("id")?, |
| 179 | note: r.try_get("note")?, |
| 180 | expires_at: r.try_get("expires_at")?, |
| 181 | used_at: r.try_get("used_at")?, |
| 182 | used_by: r.try_get("used_by")?, |
| 183 | created_at: r.try_get("created_at")?, |
| 184 | }) |
| 185 | }) |
| 186 | .collect::<Result<_, sqlx::Error>>() |
| 187 | .map_err(Into::into) |
| 188 | } |
| 189 | |
| 190 | |
| 191 | |
| 192 | |
| 193 | |
| 194 | |
| 195 | pub async fn signup_invite_valid( |
| 196 | &self, |
| 197 | token_hash: &str, |
| 198 | ) -> Result<Option<String>, StoreError> { |
| 199 | let row = sqlx::query( |
| 200 | "SELECT id FROM signup_invites \ |
| 201 | WHERE token_hash = $1 AND used_at IS NULL \ |
| 202 | AND (expires_at IS NULL OR expires_at > $2)", |
| 203 | ) |
| 204 | .bind(token_hash) |
| 205 | .bind(now_ms()) |
| 206 | .fetch_optional(&self.pool) |
| 207 | .await?; |
| 208 | Ok(row.map(|r| r.try_get("id")).transpose()?) |
| 209 | } |
| 210 | |
| 211 | |
| 212 | |
| 213 | |
| 214 | |
| 215 | |
| 216 | pub async fn redeem_signup_invite( |
| 217 | &self, |
| 218 | token_hash: &str, |
| 219 | user_id: &str, |
| 220 | ) -> Result<(), StoreError> { |
| 221 | sqlx::query( |
| 222 | "UPDATE signup_invites SET used_at = $1, used_by = $2 \ |
| 223 | WHERE token_hash = $3 AND used_at IS NULL", |
| 224 | ) |
| 225 | .bind(now_ms()) |
| 226 | .bind(user_id) |
| 227 | .bind(token_hash) |
| 228 | .execute(&self.pool) |
| 229 | .await?; |
| 230 | Ok(()) |
| 231 | } |
| 232 | |
| 233 | |
| 234 | |
| 235 | |
| 236 | |
| 237 | |
| 238 | pub async fn delete_signup_invite(&self, id: &str) -> Result<bool, StoreError> { |
| 239 | let n = sqlx::query("DELETE FROM signup_invites WHERE id = $1") |
| 240 | .bind(id) |
| 241 | .execute(&self.pool) |
| 242 | .await? |
| 243 | .rows_affected(); |
| 244 | Ok(n > 0) |
| 245 | } |
| 246 | |
| 247 | |
| 248 | |
| 249 | |
| 250 | |
| 251 | |
| 252 | |
| 253 | |
| 254 | pub async fn all_settings(&self) -> Result<HashMap<String, String>, StoreError> { |
| 255 | let rows = sqlx::query("SELECT key, value FROM instance_settings") |
| 256 | .fetch_all(&self.pool) |
| 257 | .await?; |
| 258 | let mut out = HashMap::new(); |
| 259 | for r in &rows { |
| 260 | out.insert( |
| 261 | r.try_get::<String, _>("key")?, |
| 262 | r.try_get::<String, _>("value")?, |
| 263 | ); |
| 264 | } |
| 265 | Ok(out) |
| 266 | } |
| 267 | |
| 268 | |
| 269 | |
| 270 | |
| 271 | |
| 272 | |
| 273 | pub async fn set_setting(&self, key: &str, value: &str) -> Result<(), StoreError> { |
| 274 | sqlx::query( |
| 275 | "INSERT INTO instance_settings (key, value) VALUES ($1, $2) \ |
| 276 | ON CONFLICT (key) DO UPDATE SET value = $2", |
| 277 | ) |
| 278 | .bind(key) |
| 279 | .bind(value) |
| 280 | .execute(&self.pool) |
| 281 | .await?; |
| 282 | Ok(()) |
| 283 | } |
| 284 | |
| 285 | |
| 286 | |
| 287 | |
| 288 | |
| 289 | |
| 290 | pub async fn delete_setting(&self, key: &str) -> Result<(), StoreError> { |
| 291 | sqlx::query("DELETE FROM instance_settings WHERE key = $1") |
| 292 | .bind(key) |
| 293 | .execute(&self.pool) |
| 294 | .await?; |
| 295 | Ok(()) |
| 296 | } |
| 297 | } |