feat: configurable OAuth (Google + TikTok SSO), project membership, inline file preview
- Auth: configurable OAuthProvider enum supporting Google OAuth and TikTok SSO - Auth: /auth/provider endpoint for frontend to detect active provider - Auth: user role system (admin via ADMIN_USERS env var sees all projects) - Projects: project_members many-to-many table with role (owner/member) - Projects: membership-based access control, auto-add creator as owner - Projects: member management API (list/add/remove) - Files: remove Content-Disposition attachment header, let browser decide - Health: public /tori/api/health endpoint for k8s probes
This commit is contained in:
73
src/db.rs
73
src/db.rs
@@ -249,6 +249,79 @@ impl Database {
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
// Migration: add role column to users (admin = see all projects)
|
||||
let _ = sqlx::query(
|
||||
"ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'"
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS project_members (
|
||||
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||
user_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'owner',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (project_id, user_id)
|
||||
)"
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
// Migration: assign all existing memberless projects to the first user (or leave for manual assignment)
|
||||
// When auth is not configured, owner_id is empty — these projects are visible to everyone
|
||||
// When a user logs in and creates projects, they get auto-added as admin
|
||||
{
|
||||
// Find existing projects with owner_id set but no members yet
|
||||
let owned: Vec<(String, String)> = sqlx::query_as(
|
||||
"SELECT p.id, p.owner_id FROM projects p \
|
||||
WHERE p.deleted = 0 AND p.owner_id != '' \
|
||||
AND NOT EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id)"
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
for (pid, uid) in owned {
|
||||
let _ = sqlx::query(
|
||||
"INSERT OR IGNORE INTO project_members (project_id, user_id, role) VALUES (?, ?, 'owner')"
|
||||
)
|
||||
.bind(&pid)
|
||||
.bind(&uid)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
}
|
||||
|
||||
// For orphan projects (no owner, no members), assign to first user if one exists
|
||||
let first_user: Option<(String,)> = sqlx::query_as(
|
||||
"SELECT id FROM users ORDER BY created_at ASC LIMIT 1"
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
if let Some((first_uid,)) = first_user {
|
||||
let orphans: Vec<(String,)> = sqlx::query_as(
|
||||
"SELECT p.id FROM projects p \
|
||||
WHERE p.deleted = 0 \
|
||||
AND NOT EXISTS (SELECT 1 FROM project_members pm WHERE pm.project_id = p.id)"
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
for (pid,) in orphans {
|
||||
let _ = sqlx::query(
|
||||
"INSERT OR IGNORE INTO project_members (project_id, user_id, role) VALUES (?, ?, 'owner')"
|
||||
)
|
||||
.bind(&pid)
|
||||
.bind(&first_uid)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user