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:
Fam Zheng
2026-03-17 03:42:38 +00:00
parent 63f0582f54
commit 28a00dd2f3
7 changed files with 504 additions and 98 deletions

View File

@@ -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(())
}
}