Skip to content

feat: Implement colab settings #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion backend/src/auth/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use uuid::Uuid;
use crate::auth::jwt::RgUserData;
use crate::auth::{github, jwt};
use crate::http_errors::HttpErrors;
use crate::state::AppState;

pub struct OAuthData {
pub client: oauth2::basic::BasicClient,
Expand Down Expand Up @@ -39,6 +40,7 @@ pub async fn me(req: HttpRequest) -> HttpResult<HttpErrors> {

#[get("/auth/callback")]
async fn callback(
state: web::Data<AppState>,
query: web::Query<AuthRequest>,
oauth_data: web::Data<OAuthData>,
) -> HttpResult<HttpErrors> {
Expand All @@ -62,6 +64,8 @@ async fn callback(
let jwt =
RgUserData::new(github_user.login.clone(), github_user.login.clone(), false).encode()?;

state.add_username(github_user.login.clone(), github_user.login.clone());

Ok(HttpResponse::Ok().json(serde_json::json!({
"jwt": jwt,
"id": github_user.login,
Expand All @@ -77,11 +81,16 @@ struct GuestLoginRequest {
}

#[proof_route(post("/auth/guest"))]
async fn login_guest(body: web::Json<GuestLoginRequest>) -> HttpResult<HttpErrors> {
async fn login_guest(
state: web::Data<AppState>,
body: web::Json<GuestLoginRequest>,
) -> HttpResult<HttpErrors> {
let guest_name = &body.guest_name;
let guest_uuid = Uuid::new_v4().to_string();
let jwt = RgUserData::new(guest_uuid.clone(), guest_name.clone(), true).encode()?;

state.add_username(guest_uuid.clone(), guest_name.clone());

Ok(HttpResponse::Ok().json(serde_json::json!({
"jwt": jwt,
"id": guest_uuid,
Expand All @@ -96,6 +105,7 @@ struct UpdateNameRequest {

#[proof_route(post("/auth/update"))]
async fn update_name(
state: web::Data<AppState>,
body: web::Json<UpdateNameRequest>,
req: actix_web::HttpRequest,
) -> HttpResult<HttpErrors> {
Expand All @@ -110,6 +120,8 @@ async fn update_name(

let jwt = RgUserData::new(uuid.clone(), new_name.clone(), true).encode()?;

state.add_username(uuid.clone(), new_name.clone());

Ok(HttpResponse::Ok().json(serde_json::json!({
"jwt": jwt,
"id": uuid,
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod state;
mod utils;
mod ws;

use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};

use actix_cors::Cors;
Expand Down Expand Up @@ -35,6 +36,7 @@ async fn main() -> std::io::Result<()> {

let app_state = web::Data::new(AppState {
manager: Mutex::new(ProjectManager::new()).into(),
usernames: Mutex::new(HashMap::new()).into(),
});

HttpServer::new(move || {
Expand Down
14 changes: 13 additions & 1 deletion backend/src/project/project.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};

use tokio::sync::broadcast;
use uuid::Uuid;

use crate::auth::jwt::RgUserData;
use crate::collab::{Document, DocumentInfo};
use crate::http_errors::HttpErrors;
use crate::ws::messages::ServerMessage;
Expand All @@ -15,6 +16,7 @@ pub struct Project {
pub owner: String,
pub documents: HashMap<String, Document>,
pub allowed_users: HashMap<String, AccessLevel>,
pub requests: HashSet<String>,
pub is_public: bool,
pub password: Option<String>,
pub broadcast: broadcast::Sender<ServerMessage>,
Expand All @@ -28,6 +30,7 @@ impl Default for Project {
owner: String::new(),
documents: HashMap::new(),
allowed_users: HashMap::new(),
requests: HashSet::new(),
is_public: true,
password: None,
broadcast: broadcast::channel(u8::MAX as usize).0,
Expand All @@ -44,6 +47,15 @@ impl Project {
}
}

pub fn add_request(&mut self, user_info: &RgUserData) {
if self.requests.insert(user_info.id.clone()) {
_ = self.broadcast.send(ServerMessage::RequestAccess {
user_id: user_info.id.clone(),
user_name: user_info.name.clone(),
});
}
}

pub fn permit_access(&mut self, user_id: String, access: AccessLevel) {
self.allowed_users.insert(user_id, access);
}
Expand Down
22 changes: 20 additions & 2 deletions backend/src/project/routes.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashMap;

use actix_error_proc::{proof_route, HttpResult};
use actix_web::{web, HttpRequest, HttpResponse};
use serde_json::json;
Expand All @@ -16,7 +18,11 @@ pub async fn get_project(
) -> HttpResult<HttpErrors> {
let app_state = app_state.into_inner();
let project_id = project_id.into_inner();
let password = Some(req.query_string().to_owned()).take_if(|s| !s.is_empty());
let password = req
.query_string()
.strip_prefix("p=")
.take_if(|s| !s.is_empty())
.map(|s| s.to_owned());

let user_info = jwt::get_user_info(&req)?;

Expand All @@ -28,6 +34,8 @@ pub async fn get_project(
let access = project.join_project(&user_info.id, password)?;

if !access.can_read() {
project.add_request(&user_info);

return Ok(HttpResponse::Unauthorized().json(json!({
"access": access,
"id": project.id,
Expand All @@ -36,12 +44,22 @@ pub async fn get_project(
})));
}

let users = project
.allowed_users
.iter()
.filter_map(|(user, access)| {
app_state
.get_username(&user)
.map(|username| (user, (username, access)))
})
.collect::<HashMap<&String, (String, &AccessLevel)>>();

Ok(HttpResponse::Ok().json(json!({
"access": access,
"id": project.id,
"name": project.name,
"owner": project.owner,
"allowed_users": project.allowed_users,
"users": users,
"is_public": project.is_public,
"password": project.password
})))
Expand Down
20 changes: 20 additions & 0 deletions backend/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex, MutexGuard};

use crate::project::ProjectManager;

#[derive(Clone)]
pub struct AppState {
pub manager: Arc<Mutex<ProjectManager>>,
pub usernames: Arc<Mutex<HashMap<String, String>>>,
}

impl AppState {
/// Lock project manager, and recover from poison mutex
pub fn get_manager(&self) -> MutexGuard<'_, ProjectManager> {
self.manager.lock().unwrap_or_else(|e| e.into_inner())
}

/// Get username of user with provided id
pub fn get_username(&self, id: &String) -> Option<String> {
self.usernames
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(id)
.cloned()
}

/// Insert username of user with provided id
pub fn add_username(&self, id: String, username: String) {
log::trace!("New username registered: {id:?} = {username:?}");
self.usernames
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(id, username);
}
}
80 changes: 79 additions & 1 deletion backend/src/ws/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ impl RgWebsocket {
}

pub async fn handle_welcome(&self, ctx: &mut ws::Session) {
// Don't send welcome when is in queue
if !self.access.can_read() {
return;
}

Self::handle_ws_response(ctx, self.compose_welcome().await).await
}

Expand All @@ -31,15 +36,41 @@ impl RgWebsocket {

_ = project.broadcast.send(ServerMessage::UserConnected {
user_id: self.user_info.id.clone(),
user_name: self.user_info.name.clone(),
});

let files = project.get_files().clone();
let users = project.allowed_users.clone();
let users = project
.allowed_users
.iter()
.filter_map(|(user, access)| {
self.app_state
.get_username(&user)
.map(|username| (user.clone(), (username, *access)))
})
.collect();

let requests = if project.owner == self.user_info.id {
Some(
project
.requests
.iter()
.filter_map(|user| {
self.app_state
.get_username(&user)
.map(|username| (user.clone(), username))
})
.collect(),
)
} else {
None
};

Ok(ServerMessage::Welcome {
session_id: self.session_id.clone(),
files,
users,
requests,
})
}

Expand All @@ -52,6 +83,18 @@ impl RgWebsocket {
.text_json(&ServerMessage::UpdateAccess { access, user_id })
.await;
}
// Only update requests to owner
ServerMessage::RequestAccess { .. }
if self
.app_state
.get_manager()
.get_project(&self.project_id)
.is_some_and(|p| p.owner == self.user_info.id) =>
{
_ = ctx.text_json(&msg).await
}
ServerMessage::RequestAccess { .. } => {}

_ if self.access.can_read() => _ = ctx.text_json(&msg).await,
_ => {}
}
Expand Down Expand Up @@ -104,6 +147,41 @@ impl RgWebsocket {
let mut manager = self.app_state.get_manager();

match msg {
ClientMessage::Config {
name,
is_public,
password,
} => {
let project = manager.get_project_mut(self.project_id)?;

if project.owner != self.user_info.id {
return Err(ServerMessageError::None);
}

if let Some(name) = name {
project.name = name;
}

if let Some(is_public) = is_public {
project.is_public = is_public;
}

if let Some(password) = password {
if password.is_empty() {
project.password = None;
} else {
project.password = project.is_public.then_some(password);
}
}

_ = project.broadcast.send(ServerMessage::ProjectConfig {
name: project.name.clone(),
is_public: project.is_public,
password: project.password.clone(),
});

Err(ServerMessageError::None)
}
ClientMessage::FileCreate { file } => {
self.access.need_editor()?;

Expand Down
25 changes: 21 additions & 4 deletions backend/src/ws/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ use crate::project::AccessLevel;
#[derive(Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum ClientMessage {
PermitAccess {
user_id: String,
access: AccessLevel,
Config {
name: Option<String>,
is_public: Option<bool>,
password: Option<String>,
},
FileCreate {
file: String,
},
FileDelete {
file: String,
},
PermitAccess {
user_id: String,
access: AccessLevel,
},
Sync {
file: String,
revision: usize,
Expand All @@ -37,6 +42,11 @@ pub enum ServerMessage {
Error {
message: String,
},
ProjectConfig {
name: String,
is_public: bool,
password: Option<String>,
},
ProjectFiles {
/// List of all file paths
files: HashMap<String, DocumentInfo>,
Expand All @@ -47,6 +57,11 @@ pub enum ServerMessage {
},
UserConnected {
user_id: String,
user_name: String,
},
RequestAccess {
user_id: String,
user_name: String,
},
Sync {
file: String,
Expand All @@ -60,7 +75,9 @@ pub enum ServerMessage {
Welcome {
session_id: String,
files: HashMap<String, DocumentInfo>,
users: HashMap<String, AccessLevel>,
users: HashMap<String, (String, AccessLevel)>,
// Only for owner
requests: Option<HashMap<String, String>>,
},
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/SelectField.module.sass
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
color: var(--surface2-foreground)
font-size: .8rem

&:hover
&:where(:not(.disabled)):hover
filter: brightness(0.8)

> span
Expand Down
Loading
Loading