Skip to content

Commit

Permalink
feat(webserver): Add authenticated endpoint to update password (Tabby…
Browse files Browse the repository at this point in the history
…ML#1553)

* feat(webserver): Add authenticated endpoint to update password

* [autofix.ci] apply automated fixes

* Apply suggestions

* [autofix.ci] apply automated fixes

* Make password optional

* Add todo

* [autofix.ci] apply automated fixes

* switch to id

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Meng Zhang <[email protected]>
  • Loading branch information
3 people authored Feb 27, 2024
1 parent 0e6eec4 commit d0836db
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 9 deletions.
24 changes: 16 additions & 8 deletions ee/tabby-webserver/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Mutation {
requestInvitationEmail(input: RequestInvitationInput!): Invitation!
requestPasswordResetEmail(input: RequestPasswordResetEmailInput!): Boolean!
passwordReset(input: PasswordResetInput!): Boolean!
passwordChange(input: PasswordUpdateInput!): Boolean!
resetUserAuthToken: Boolean!
updateUserActive(id: ID!, active: Boolean!): Boolean!
updateUserRole(id: ID!, isAdmin: Boolean!): Boolean!
Expand Down Expand Up @@ -110,9 +111,10 @@ type RefreshTokenResponse {
refreshExpiresAt: DateTimeUtc!
}

type RegisterResponse {
accessToken: String!
refreshToken: String!
input PasswordUpdateInput {
oldPassword: String
newPassword1: String!
newPassword2: String!
}

type RepositoryConnection {
Expand All @@ -139,6 +141,11 @@ type LicenseInfo {
expiresAt: DateTimeUtc
}

type RegisterResponse {
accessToken: String!
refreshToken: String!
}

input EmailSettingInput {
smtpUsername: String!
fromAddress: String!
Expand All @@ -149,11 +156,6 @@ input EmailSettingInput {
smtpPassword: String
}

input SecuritySettingInput {
allowedRegisterDomainList: [String!]!
disableClientSideTelemetry: Boolean!
}

enum LicenseType {
COMMUNITY
TEAM
Expand All @@ -176,6 +178,11 @@ input UpdateOAuthCredentialInput {
clientSecret: String
}

input SecuritySettingInput {
allowedRegisterDomainList: [String!]!
disableClientSideTelemetry: Boolean!
}

type OAuthCredential {
provider: OAuthProvider!
clientId: String!
Expand Down Expand Up @@ -221,6 +228,7 @@ type User {
authToken: String!
createdAt: DateTimeUtc!
active: Boolean!
isPasswordSet: Boolean!
}

type Worker {
Expand Down
40 changes: 40 additions & 0 deletions ee/tabby-webserver/src/schema/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ pub struct User {
pub auth_token: String,
pub created_at: DateTime<Utc>,
pub active: bool,
pub is_password_set: bool,
}

impl relay::NodeType for User {
Expand Down Expand Up @@ -306,6 +307,39 @@ pub struct PasswordResetInput {
pub password2: String,
}

#[derive(Validate, GraphQLInputObject)]
pub struct PasswordUpdateInput {
pub old_password: Option<String>,

#[validate(length(
min = 8,
code = "new_password1",
message = "Password must be at least 8 characters"
))]
#[validate(length(
max = 20,
code = "new_password1",
message = "Password must be at most 20 characters"
))]
pub new_password1: String,
#[validate(length(
min = 8,
code = "new_password2",
message = "Password must be at least 8 characters"
))]
#[validate(length(
max = 20,
code = "new_password2",
message = "Password must be at most 20 characters"
))]
#[validate(must_match(
code = "new_password2",
message = "Passwords do not match",
other = "new_password1"
))]
pub new_password2: String,
}

#[derive(Debug, Serialize, Deserialize, GraphQLObject)]
#[graphql(context = Context)]
pub struct Invitation {
Expand Down Expand Up @@ -392,6 +426,12 @@ pub trait AuthenticationService: Send + Sync {
async fn reset_user_auth_token(&self, id: &ID) -> Result<()>;
async fn password_reset(&self, code: &str, password: &str) -> Result<()>;
async fn request_password_reset_email(&self, email: String) -> Result<Option<JoinHandle<()>>>;
async fn update_user_password(
&self,
id: &ID,
old_password: Option<&str>,
new_password: &str,
) -> Result<()>;

async fn list_users(
&self,
Expand Down
17 changes: 16 additions & 1 deletion ee/tabby-webserver/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use worker::{Worker, WorkerService};

use self::{
auth::{
JWTPayload, OAuthCredential, OAuthProvider, PasswordResetInput, RequestInvitationInput,
PasswordResetInput, PasswordUpdateInput, RequestInvitationInput,
RequestPasswordResetEmailInput, UpdateOAuthCredentialInput,
},
email::{EmailService, EmailSetting, EmailSettingInput},
Expand All @@ -38,6 +38,7 @@ use self::{
NetworkSetting, NetworkSettingInput, SecuritySetting, SecuritySettingInput, SettingService,
},
};
use crate::schema::auth::{JWTPayload, OAuthCredential, OAuthProvider};

pub trait ServiceLocator: Send + Sync {
fn auth(&self) -> Arc<dyn AuthenticationService>;
Expand Down Expand Up @@ -366,6 +367,20 @@ impl Mutation {
Ok(true)
}

async fn password_change(ctx: &Context, input: PasswordUpdateInput) -> Result<bool> {
let claims = check_claims(ctx)?;
input.validate()?;
ctx.locator
.auth()
.update_user_password(
&claims.sub.0,
input.old_password.as_deref(),
&input.new_password1,
)
.await?;
Ok(true)
}

async fn reset_user_auth_token(ctx: &Context) -> Result<bool> {
let claims = check_claims(ctx)?;
ctx.locator
Expand Down
56 changes: 56 additions & 0 deletions ee/tabby-webserver/src/service/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,35 @@ impl AuthenticationService for AuthenticationServiceImpl {
Ok(())
}

async fn update_user_password(
&self,
id: &ID,
old_password: Option<&str>,
new_password: &str,
) -> Result<()> {
let user = self
.db
.get_user(id.as_rowid()?)
.await?
.ok_or_else(|| anyhow!("Invalid user"))?;

let password_verified = match (user.password_encrypted.is_empty(), old_password) {
(true, _) => true,
(false, None) => false,
(false, Some(old_password)) => password_verify(old_password, &user.password_encrypted),
};
if !password_verified {
return Err(anyhow!("Password is incorrect").into());
}

let new_password_encrypted =
password_hash(new_password).map_err(|_| anyhow!("Unknown error"))?;
self.db
.update_user_password(user.id, new_password_encrypted)
.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 {
return Err(anyhow!("User not found").into());
Expand Down Expand Up @@ -1165,4 +1194,31 @@ mod tests {
Err(CoreError::InvalidLicense(_))
);
}

#[tokio::test]
async fn test_update_password() {
let service = test_authentication_service().await;
let id = service
.db
.create_user("[email protected]".into(), "".into(), true)
.await
.unwrap();

let id = id.as_id();

assert!(service
.update_user_password(&id, None, "newpass")
.await
.is_ok());

assert!(service
.update_user_password(&id, None, "newpass2")
.await
.is_err());

assert!(service
.update_user_password(&id, Some("newpass"), "newpass2")
.await
.is_ok());
}
}
1 change: 1 addition & 0 deletions ee/tabby-webserver/src/service/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_token: val.auth_token,
created_at: val.created_at,
active: val.active,
is_password_set: !val.password_encrypted.is_empty(),
}
}
}
Expand Down

0 comments on commit d0836db

Please sign in to comment.