Skip to content
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

feat(ee): add name field for user model, add api to update it #2100

Merged
merged 10 commits into from
May 16, 2024
Merged
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,13 @@ brew install protobuf
apt-get install protobuf-compiler libopenblas-dev
```

3. Now, you can build Tabby by running the command `cargo build`.
3. Install useful tools:
```bash
# For Ubuntu
sudo apt install make sqlite3 graphviz
```

4. Now, you can build Tabby by running the command `cargo build`.

### Start Hacking!
... and don't forget to submit a [Pull Request](https://github.com/TabbyML/tabby/compare)
Expand Down
1 change: 1 addition & 0 deletions ee/tabby-db/migrations/0030_user-name.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN name;
1 change: 1 addition & 0 deletions ee/tabby-db/migrations/0030_user-name.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN name VARCHAR(255);
Binary file modified ee/tabby-db/schema.sqlite
Binary file not shown.
1 change: 1 addition & 0 deletions ee/tabby-db/schema/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ CREATE TABLE users(
active BOOLEAN NOT NULL DEFAULT 1,
password_encrypted VARCHAR(128),
avatar BLOB DEFAULT NULL,
name VARCHAR(255),
CONSTRAINT `idx_email` UNIQUE(`email`)
CONSTRAINT `idx_auth_token` UNIQUE(`auth_token`)
);
Expand Down
4 changes: 4 additions & 0 deletions ee/tabby-db/schema/schema.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 24 additions & 1 deletion ee/tabby-db/src/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct UserDAO {

pub id: i64,
pub email: String,
pub name: Option<String>,
pub password_encrypted: Option<String>,
pub is_admin: bool,

Expand All @@ -28,7 +29,7 @@ macro_rules! select {
($str:literal $(,)? $($val:expr),*) => {
query_as!(
UserDAO,
r#"SELECT id as "id!", email, password_encrypted, is_admin, created_at as "created_at!", updated_at as "updated_at!", auth_token, active FROM users WHERE "# + $str,
r#"SELECT id as "id!", email, name, password_encrypted, is_admin, created_at as "created_at!", updated_at as "updated_at!", auth_token, active FROM users WHERE "# + $str,
$($val),*
)
}
Expand Down Expand Up @@ -123,6 +124,7 @@ impl DbConn {
[
"id"!,
"email",
"name",
"password_encrypted",
"is_admin",
"created_at"!,
Expand Down Expand Up @@ -256,6 +258,13 @@ impl DbConn {
})
.await
}

pub async fn update_user_name(&self, id: i64, name: String) -> Result<()> {
query!("UPDATE users SET name = ? WHERE id = ?;", name, id)
.execute(&self.pool)
.await?;
Ok(())
}
}

fn generate_auth_token() -> String {
Expand Down Expand Up @@ -292,6 +301,20 @@ mod tests {
assert!(conn.update_user_active(id, false).await.is_err());
}

#[tokio::test]
async fn test_update_user_name() {
let conn = DbConn::new_in_memory().await.unwrap();
let id = create_user(&conn).await;

let user = conn.get_user(id).await.unwrap().unwrap();
assert_eq!(user.name, None);

conn.update_user_name(id, "test".into()).await.unwrap();

let user = conn.get_user(id).await.unwrap().unwrap();
assert_eq!(user.name, Some("test".into()));
}

#[tokio::test]
async fn test_get_user_by_email() {
let conn = DbConn::new_in_memory().await.unwrap();
Expand Down
2 changes: 2 additions & 0 deletions ee/tabby-schema/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ type Mutation {
updateUserActive(id: ID!, active: Boolean!): Boolean!
updateUserRole(id: ID!, isAdmin: Boolean!): Boolean!
uploadUserAvatarBase64(id: ID!, avatarBase64: String): Boolean!
updateUserName(id: ID!, name: String!): Boolean!
register(email: String!, password1: String!, password2: String!, invitationCode: String): RegisterResponse!
tokenAuth(email: String!, password: String!): TokenAuthResponse!
verifyToken(token: String!): Boolean!
Expand Down Expand Up @@ -445,6 +446,7 @@ type TokenAuthResponse {
type User {
id: ID!
email: String!
name: String!
isAdmin: Boolean!
isOwner: Boolean!
authToken: String!
Expand Down
1 change: 1 addition & 0 deletions ee/tabby-schema/src/dao.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ impl From<UserDAO> for auth::User {
auth::User {
id: val.id.as_id(),
email: val.email,
name: val.name.unwrap_or_default(),
is_owner,
is_admin: val.is_admin,
auth_token: val.auth_token,
Expand Down
18 changes: 18 additions & 0 deletions ee/tabby-schema/src/schema/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ impl JWTPayload {
pub struct User {
pub id: juniper::ID,
pub email: String,
pub name: String,
pub is_admin: bool,
pub is_owner: bool,
pub auth_token: String,
Expand Down Expand Up @@ -278,6 +279,22 @@ pub struct PasswordChangeInput {
pub new_password2: String,
}

#[derive(Validate)]
pub struct UpdateUserNameInput {
#[validate(length(min = 2, code = "name", message = "Name must be at least 2 characters"))]
#[validate(length(
max = 20,
code = "name",
message = "Name must be at most 20 characters"
))]
#[validate(regex(
code = "name",
path = "crate::schema::constants::USERNAME_REGEX",
message = "Invalid name, name may contain numbers or special characters which are not supported"
))]
pub name: String,
}

#[derive(Debug, Serialize, Deserialize, GraphQLObject)]
#[graphql(context = Context)]
pub struct Invitation {
Expand Down Expand Up @@ -406,6 +423,7 @@ pub trait AuthenticationService: Send + Sync {
async fn update_user_role(&self, id: &ID, is_admin: bool) -> Result<()>;
async fn update_user_avatar(&self, id: &ID, avatar: Option<Box<[u8]>>) -> Result<()>;
async fn get_user_avatar(&self, id: &ID) -> Result<Option<Box<[u8]>>>;
async fn update_user_name(&self, id: &ID, name: String) -> Result<()>;
}

fn validate_password(value: &str) -> Result<(), validator::ValidationError> {
Expand Down
37 changes: 37 additions & 0 deletions ee/tabby-schema/src/schema/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,41 @@ use regex::Regex;

lazy_static! {
pub static ref REPOSITORY_NAME_REGEX: Regex = Regex::new("^[a-zA-Z][\\w.-]+$").unwrap();
pub static ref USERNAME_REGEX: Regex =
Regex::new(r"^[^0-9±!@£$%^&*_+§¡€#¢¶•ªº«\\/<>?:;|=.,]{2,20}$").unwrap();
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_username_regex() {
let test_cases = vec![
("John", true), // English name
("Müller", true), // German name
("Jørgensen", true), // Danish name
("李雷", true), // Chinese name
("あきは", true), // Japanese name
("김민수", true), // Korean name
("Алексей", true), // Russian name
("José", true), // Spanish names
("علی", true), // Iranian names
// Edge cases
("", false), // Empty string
("JohnDoeIsAReallyLongName", false), // More than 20 characters
("John!", false), // Invalid character '!'
("José@", false), // Invalid character '@'
("12345", false), // Invalid character Numbers
("John_Doe", false), // Underscore character
("Anna-Marie", true), // Hyphen character
("O'Connor", true), // Apostrophe
("李@伟", false),
];

for (name, expected) in test_cases {
let result = USERNAME_REGEX.is_match(name);
assert_eq!(result, expected, "Failed for name: {}", name);
}
}
}
13 changes: 13 additions & 0 deletions ee/tabby-schema/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,19 @@ impl Mutation {
Ok(true)
}

async fn update_user_name(ctx: &Context, id: ID, name: String) -> Result<bool> {
let claims = check_claims(ctx)?;
if claims.sub != id {
return Err(CoreError::Unauthorized(
"You cannot change another user's name",
));
}
let input = auth::UpdateUserNameInput { name };
input.validate()?;
ctx.locator.auth().update_user_name(&id, input.name).await?;
Ok(true)
}

async fn register(
ctx: &Context,
email: String,
Expand Down
9 changes: 9 additions & 0 deletions ee/tabby-webserver/src/service/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,15 @@ impl AuthenticationService for AuthenticationServiceImpl {
Ok(self.db.get_user_avatar(id.as_rowid()?).await?)
}

async fn update_user_name(&self, id: &ID, name: String) -> Result<()> {
if is_demo_mode() {
bail!("Changing profile data is disabled in demo mode");
}
let id = id.as_rowid()?;
self.db.update_user_name(id, name).await?;
Ok(())
}

async fn token_auth(&self, email: String, password: String) -> Result<TokenAuthResponse> {
let Some(user) = self.db.get_user_by_email(&email).await? else {
bail!("Invalid email address or password");
Expand Down
Loading