Skip to content

Commit 7d3c2c7

Browse files
committed
Add user scoping, ASSETS binding check, and improved display
Features: - Add --user flag to DB alias for user-scoped operations - Require ASSETS binding for upload (match API behavior) - Show environment name in worker display - Add env bind command for assets/storage/kv/database bindings - Show binding types in env list/get ([assets], [kv], etc.) - Add folder upload support (auto-zip) - Add setup-storage top-level command for platform storage Fixes: - Upload now checks for ASSETS binding instead of auto-creating - Use binding's storage credentials with platform endpoint fallback - Proper user lookup by username (supports OAuth users)
1 parent b39d576 commit 7d3c2c7

9 files changed

Lines changed: 429 additions & 100 deletions

File tree

src/backend/db.rs

Lines changed: 99 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use super::{
44
EnvironmentValue, KvNamespace, StorageConfig, UpdateEnvironmentInput, UpdateWorkerInput,
55
UploadResult, UploadWorkerInfo, UploadedCounts, Worker,
66
};
7+
use crate::config::PlatformStorageConfig;
78
use crate::s3::{S3Client, S3Config, get_mime_type};
89
use sha2::{Digest, Sha256};
910
use sqlx::{PgPool, Row};
@@ -13,23 +14,38 @@ use zip::ZipArchive;
1314
pub struct DbBackend {
1415
pool: PgPool,
1516
user_id: uuid::Uuid,
17+
platform_storage: Option<PlatformStorageConfig>,
1618
}
1719

1820
impl DbBackend {
19-
pub async fn new(pool: PgPool) -> Result<Self, BackendError> {
20-
// Get or create admin user on initialization
21-
let user_id: uuid::Uuid = sqlx::query_scalar(
22-
r#"
23-
INSERT INTO users (id, username, created_at, updated_at)
24-
VALUES (gen_random_uuid(), 'cli-admin', now(), now())
25-
ON CONFLICT (username) DO UPDATE SET username = users.username
26-
RETURNING id
27-
"#,
28-
)
29-
.fetch_one(&pool)
30-
.await?;
21+
pub async fn new(
22+
pool: PgPool,
23+
username: Option<String>,
24+
platform_storage: Option<PlatformStorageConfig>,
25+
) -> Result<Self, BackendError> {
26+
let username = username.ok_or_else(|| {
27+
BackendError::Api(
28+
"No user configured for this DB alias. Use 'ow alias set <name> --db <url> --user <username>' to set a user.".to_string(),
29+
)
30+
})?;
3131

32-
Ok(Self { pool, user_id })
32+
// Look up user by username
33+
let user_id: uuid::Uuid = sqlx::query_scalar("SELECT id FROM users WHERE username = $1")
34+
.bind(&username)
35+
.fetch_optional(&pool)
36+
.await?
37+
.ok_or_else(|| {
38+
BackendError::NotFound(format!(
39+
"User '{}' not found. Create an account first via the dashboard.",
40+
username
41+
))
42+
})?;
43+
44+
Ok(Self {
45+
pool,
46+
user_id,
47+
platform_storage,
48+
})
3349
}
3450

3551
async fn get_environment_values(
@@ -66,10 +82,12 @@ impl Backend for DbBackend {
6682
async fn list_workers(&self) -> Result<Vec<Worker>, BackendError> {
6783
let rows = sqlx::query(
6884
r#"
69-
SELECT id, name, "desc", current_version, created_at, updated_at
70-
FROM workers
71-
WHERE user_id = $1
72-
ORDER BY name
85+
SELECT w.id, w.name, w."desc", w.current_version, w.created_at, w.updated_at,
86+
e.id as env_id, e.name as env_name
87+
FROM workers w
88+
LEFT JOIN environments e ON e.id = w.environment_id
89+
WHERE w.user_id = $1
90+
ORDER BY w.name
7391
"#,
7492
)
7593
.bind(self.user_id)
@@ -78,13 +96,26 @@ impl Backend for DbBackend {
7896

7997
let workers = rows
8098
.iter()
81-
.map(|row| Worker {
82-
id: row.get::<uuid::Uuid, _>("id").to_string(),
83-
name: row.get("name"),
84-
description: row.get("desc"),
85-
current_version: row.get("current_version"),
86-
created_at: row.get("created_at"),
87-
updated_at: row.get("updated_at"),
99+
.map(|row| {
100+
let env_id: Option<uuid::Uuid> = row.get("env_id");
101+
let env_name: Option<String> = row.get("env_name");
102+
let environment =
103+
env_id
104+
.zip(env_name)
105+
.map(|(id, name)| super::WorkerEnvironmentRef {
106+
id: id.to_string(),
107+
name,
108+
});
109+
110+
Worker {
111+
id: row.get::<uuid::Uuid, _>("id").to_string(),
112+
name: row.get("name"),
113+
description: row.get("desc"),
114+
current_version: row.get("current_version"),
115+
environment,
116+
created_at: row.get("created_at"),
117+
updated_at: row.get("updated_at"),
118+
}
88119
})
89120
.collect();
90121

@@ -94,9 +125,11 @@ impl Backend for DbBackend {
94125
async fn get_worker(&self, name: &str) -> Result<Worker, BackendError> {
95126
let row = sqlx::query(
96127
r#"
97-
SELECT id, name, "desc", current_version, created_at, updated_at
98-
FROM workers
99-
WHERE name = $1 AND user_id = $2
128+
SELECT w.id, w.name, w."desc", w.current_version, w.created_at, w.updated_at,
129+
e.id as env_id, e.name as env_name
130+
FROM workers w
131+
LEFT JOIN environments e ON e.id = w.environment_id
132+
WHERE w.name = $1 AND w.user_id = $2
100133
"#,
101134
)
102135
.bind(name)
@@ -105,11 +138,21 @@ impl Backend for DbBackend {
105138
.await?
106139
.ok_or_else(|| BackendError::NotFound(format!("Worker '{}' not found", name)))?;
107140

141+
let env_id: Option<uuid::Uuid> = row.get("env_id");
142+
let env_name: Option<String> = row.get("env_name");
143+
let environment = env_id
144+
.zip(env_name)
145+
.map(|(id, name)| super::WorkerEnvironmentRef {
146+
id: id.to_string(),
147+
name,
148+
});
149+
108150
Ok(Worker {
109151
id: row.get::<uuid::Uuid, _>("id").to_string(),
110152
name: row.get("name"),
111153
description: row.get("desc"),
112154
current_version: row.get("current_version"),
155+
environment,
113156
created_at: row.get("created_at"),
114157
updated_at: row.get("updated_at"),
115158
})
@@ -136,6 +179,7 @@ impl Backend for DbBackend {
136179
name: row.get("name"),
137180
description: row.get("desc"),
138181
current_version: row.get("current_version"),
182+
environment: None,
139183
created_at: row.get("created_at"),
140184
updated_at: row.get("updated_at"),
141185
})
@@ -186,30 +230,30 @@ impl Backend for DbBackend {
186230
None
187231
};
188232

189-
let row = sqlx::query(
233+
let result = sqlx::query(
190234
r#"
191235
UPDATE workers
192236
SET environment_id = COALESCE($2, environment_id),
193237
updated_at = now()
194238
WHERE name = $1 AND user_id = $3
195-
RETURNING id, name, "desc", current_version, created_at, updated_at
239+
RETURNING id
196240
"#,
197241
)
198242
.bind(name)
199243
.bind(env_id)
200244
.bind(self.user_id)
201245
.fetch_optional(&self.pool)
202-
.await?
203-
.ok_or_else(|| BackendError::NotFound(format!("Worker '{}' not found", name)))?;
246+
.await?;
204247

205-
Ok(Worker {
206-
id: row.get::<uuid::Uuid, _>("id").to_string(),
207-
name: row.get("name"),
208-
description: row.get("desc"),
209-
current_version: row.get("current_version"),
210-
created_at: row.get("created_at"),
211-
updated_at: row.get("updated_at"),
212-
})
248+
if result.is_none() {
249+
return Err(BackendError::NotFound(format!(
250+
"Worker '{}' not found",
251+
name
252+
)));
253+
}
254+
255+
// Fetch updated worker with environment info
256+
self.get_worker(name).await
213257
}
214258

215259
async fn deploy_worker(
@@ -284,11 +328,10 @@ impl Backend for DbBackend {
284328
.parse()
285329
.map_err(|_| BackendError::Api(format!("Invalid worker ID: {}", worker.id)))?;
286330

287-
// 2. Get ASSETS binding for this worker
331+
// 2. Check for ASSETS binding (like API does)
288332
let assets_binding = sqlx::query(
289333
r#"
290334
SELECT
291-
sc.id as storage_config_id,
292335
sc.bucket,
293336
sc.prefix,
294337
sc.access_key_id,
@@ -298,11 +341,12 @@ impl Backend for DbBackend {
298341
FROM workers w
299342
JOIN environment_values ev ON ev.environment_id = w.environment_id
300343
JOIN storage_configs sc ON sc.id = ev.value::uuid
301-
WHERE w.id = $1 AND ev.type = 'assets'
344+
WHERE w.id = $1 AND w.user_id = $2 AND ev.type = 'assets'
302345
LIMIT 1
303346
"#,
304347
)
305348
.bind(worker_id)
349+
.bind(self.user_id)
306350
.fetch_optional(&self.pool)
307351
.await?
308352
.ok_or_else(|| {
@@ -311,17 +355,22 @@ impl Backend for DbBackend {
311355
)
312356
})?;
313357

358+
// 3. Get storage credentials from binding, with platform endpoint as fallback
314359
let bucket: String = assets_binding.get("bucket");
315360
let prefix: Option<String> = assets_binding.get("prefix");
316361
let access_key_id: String = assets_binding.get("access_key_id");
317362
let secret_access_key: String = assets_binding.get("secret_access_key");
318-
let endpoint: Option<String> = assets_binding.get("endpoint");
319-
let region: Option<String> = assets_binding.get("region");
320-
321-
let endpoint = endpoint
363+
let region: String = assets_binding
364+
.get::<Option<String>, _>("region")
365+
.unwrap_or_else(|| "auto".to_string());
366+
367+
// Use binding's endpoint, or fall back to platform storage endpoint
368+
let binding_endpoint: Option<String> = assets_binding.get("endpoint");
369+
let endpoint = binding_endpoint
370+
.or_else(|| self.platform_storage.as_ref().map(|ps| ps.endpoint.clone()))
322371
.ok_or_else(|| BackendError::Api("Storage endpoint not configured".to_string()))?;
323372

324-
// 3. Extract zip
373+
// 4. Extract zip
325374
let cursor = std::io::Cursor::new(zip_data);
326375
let mut archive = ZipArchive::new(cursor)
327376
.map_err(|e| BackendError::Api(format!("Failed to read zip archive: {}", e)))?;
@@ -389,7 +438,7 @@ impl Backend for DbBackend {
389438
BackendError::Api("No worker.js or worker.ts found in zip archive".to_string())
390439
})?;
391440

392-
// 4. Update worker script in DB
441+
// 5. Update worker script in DB
393442
let script_bytes = script.as_bytes();
394443
let mut hasher = Sha256::new();
395444
hasher.update(script_bytes);
@@ -426,13 +475,13 @@ impl Backend for DbBackend {
426475
.execute(&self.pool)
427476
.await?;
428477

429-
// 5. Upload assets to S3
478+
// 6. Upload assets to S3
430479
let s3_client = S3Client::new(S3Config {
431480
bucket,
432481
endpoint,
433482
access_key_id,
434483
secret_access_key,
435-
region: region.unwrap_or_else(|| "auto".to_string()),
484+
region,
436485
prefix,
437486
});
438487

src/backend/mock.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ impl MockBackend {
3232
name: name.to_string(),
3333
description: description.map(|s| s.to_string()),
3434
current_version: None,
35+
environment: None,
3536
created_at: Utc::now(),
3637
updated_at: Utc::now(),
3738
};
@@ -49,6 +50,7 @@ impl MockBackend {
4950
name: name.to_string(),
5051
description: None,
5152
current_version: Some(version),
53+
environment: None,
5254
created_at: Utc::now(),
5355
updated_at: Utc::now(),
5456
};
@@ -94,6 +96,7 @@ impl Backend for MockBackend {
9496
name: input.name.clone(),
9597
description: input.description,
9698
current_version: None,
99+
environment: None,
97100
created_at: Utc::now(),
98101
updated_at: Utc::now(),
99102
};

src/backend/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ pub enum BackendError {
2626
Unauthorized,
2727
}
2828

29+
#[derive(Debug, Clone, Serialize, Deserialize)]
30+
#[serde(rename_all = "camelCase")]
31+
pub struct WorkerEnvironmentRef {
32+
pub id: String,
33+
pub name: String,
34+
}
35+
2936
#[derive(Debug, Clone, Serialize, Deserialize)]
3037
#[serde(rename_all = "camelCase")]
3138
pub struct Worker {
@@ -34,6 +41,7 @@ pub struct Worker {
3441
#[serde(alias = "desc")]
3542
pub description: Option<String>,
3643
pub current_version: Option<i32>,
44+
pub environment: Option<WorkerEnvironmentRef>,
3745
pub created_at: DateTime<Utc>,
3846
pub updated_at: DateTime<Utc>,
3947
}

src/commands/alias.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ pub enum AliasCommand {
2525
#[arg(long, conflicts_with = "api")]
2626
db: Option<String>,
2727

28+
/// User email or username to operate as (for DB backend)
29+
#[arg(long, requires = "db")]
30+
user: Option<String>,
31+
2832
/// Overwrite existing alias
2933
#[arg(short, long)]
3034
force: bool,
@@ -57,8 +61,9 @@ impl AliasCommand {
5761
token,
5862
insecure,
5963
db,
64+
user,
6065
force,
61-
} => cmd_set(name, api, token, insecure, db, force),
66+
} => cmd_set(name, api, token, insecure, db, user, force),
6267
Self::List => cmd_list(),
6368
Self::Remove { name } => cmd_remove(name),
6469
Self::SetDefault { name } => cmd_set_default(name),
@@ -72,13 +77,14 @@ fn cmd_set(
7277
token: Option<String>,
7378
insecure: bool,
7479
db: Option<String>,
80+
user: Option<String>,
7581
force: bool,
7682
) -> Result<(), ConfigError> {
7783
let mut config = Config::load()?;
7884

7985
let alias_config = match (api, db) {
8086
(Some(url), None) => AliasConfig::api(url, token, insecure),
81-
(None, Some(database_url)) => AliasConfig::db(database_url),
87+
(None, Some(database_url)) => AliasConfig::db(database_url, user, None),
8288
_ => {
8389
eprintln!(
8490
"{} Either --api or --db must be specified",
@@ -102,7 +108,7 @@ fn cmd_set(
102108
name.green().bold(),
103109
match alias_config {
104110
AliasConfig::Api { url, .. } => url,
105-
AliasConfig::Db { database_url } => mask_password(&database_url),
111+
AliasConfig::Db { database_url, .. } => mask_password(&database_url),
106112
}
107113
);
108114

@@ -136,7 +142,26 @@ fn cmd_list() -> Result<(), ConfigError> {
136142
let auth = if token.is_some() { " (auth)" } else { "" };
137143
("api".cyan(), format!("{}{}", url, auth.dimmed()))
138144
}
139-
AliasConfig::Db { database_url } => ("db".yellow(), mask_password(database_url)),
145+
AliasConfig::Db {
146+
database_url,
147+
user,
148+
storage,
149+
} => {
150+
let user_info = user
151+
.as_ref()
152+
.map(|u| format!(" @{}", u))
153+
.unwrap_or_default();
154+
let storage_info = if storage.is_some() { " (storage)" } else { "" };
155+
(
156+
"db".yellow(),
157+
format!(
158+
"{}{}{}",
159+
mask_password(database_url),
160+
user_info.cyan(),
161+
storage_info.dimmed()
162+
),
163+
)
164+
}
140165
};
141166

142167
println!(

0 commit comments

Comments
 (0)