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

@@ -123,25 +123,41 @@ async fn main() -> anyhow::Result<()> {
let obj_root = std::env::var("OBJ_ROOT").unwrap_or_else(|_| "/data/obj".to_string());
let auth_config = match (
std::env::var("GOOGLE_CLIENT_ID"),
std::env::var("GOOGLE_CLIENT_SECRET"),
) {
(Ok(client_id), Ok(client_secret)) => {
let jwt_secret = std::env::var("JWT_SECRET")
.unwrap_or_else(|_| uuid::Uuid::new_v4().to_string());
let public_url = std::env::var("PUBLIC_URL")
.unwrap_or_else(|_| "https://tori.euphon.cloud".to_string());
tracing::info!("Google OAuth enabled (public_url={})", public_url);
let auth_config = {
let jwt_secret = std::env::var("JWT_SECRET")
.unwrap_or_else(|_| uuid::Uuid::new_v4().to_string());
let public_url = std::env::var("PUBLIC_URL")
.unwrap_or_else(|_| "https://tori.euphon.cloud".to_string());
// Try TikTok SSO first, then Google OAuth
if let (Ok(id), Ok(secret)) = (
std::env::var("SSO_CLIENT_ID"),
std::env::var("SSO_CLIENT_SECRET"),
) {
tracing::info!("TikTok SSO enabled (public_url={})", public_url);
Some(api::auth::AuthConfig {
google_client_id: client_id,
google_client_secret: client_secret,
provider: api::auth::OAuthProvider::TikTokSso {
client_id: id,
client_secret: secret,
},
jwt_secret,
public_url,
})
}
_ => {
tracing::warn!("GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET not set, auth disabled");
} else if let (Ok(id), Ok(secret)) = (
std::env::var("GOOGLE_CLIENT_ID"),
std::env::var("GOOGLE_CLIENT_SECRET"),
) {
tracing::info!("Google OAuth enabled (public_url={})", public_url);
Some(api::auth::AuthConfig {
provider: api::auth::OAuthProvider::Google {
client_id: id,
client_secret: secret,
},
jwt_secret,
public_url,
})
} else {
tracing::warn!("No OAuth configured (set SSO_CLIENT_ID/SSO_CLIENT_SECRET or GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET)");
None
}
};
@@ -156,6 +172,10 @@ async fn main() -> anyhow::Result<()> {
});
let app = Router::new()
// Health check (public, for k8s probes)
.route("/tori/api/health", axum::routing::get(|| async {
axum::Json(serde_json::json!({"status": "ok"}))
}))
// Auth routes are public
.nest("/tori/api/auth", api::auth::router(state.clone()))
// Protected API routes