feat(webserver): implement is_admin_initialized graphql api (#929)
* feat(webserver): implement is_admin_initialized graphql api * refactor * add unit test * [autofix.ci] apply automated fixes * renaming * refactor: server -> locator * fix unused --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>add-signin-page
parent
5c52a71f77
commit
88e5187b57
|
|
@ -28,7 +28,7 @@ use axum::{
|
||||||
use hyper::Body;
|
use hyper::Body;
|
||||||
use juniper_axum::{graphiql, graphql, playground};
|
use juniper_axum::{graphiql, graphql, playground};
|
||||||
use schema::{
|
use schema::{
|
||||||
worker::{RegisterWorkerError, Worker, WorkerKind, WorkerService},
|
worker::{RegisterWorkerError, Worker, WorkerKind},
|
||||||
Schema, ServiceLocator,
|
Schema, ServiceLocator,
|
||||||
};
|
};
|
||||||
use service::create_service_locator;
|
use service::create_service_locator;
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ pub trait AuthenticationService: Send + Sync {
|
||||||
async fn token_auth(&self, email: String, password: String) -> FieldResult<TokenAuthResponse>;
|
async fn token_auth(&self, email: String, password: String) -> FieldResult<TokenAuthResponse>;
|
||||||
async fn refresh_token(&self, refresh_token: String) -> FieldResult<RefreshTokenResponse>;
|
async fn refresh_token(&self, refresh_token: String) -> FieldResult<RefreshTokenResponse>;
|
||||||
async fn verify_token(&self, access_token: String) -> FieldResult<VerifyTokenResponse>;
|
async fn verify_token(&self, access_token: String) -> FieldResult<VerifyTokenResponse>;
|
||||||
|
async fn is_admin_initialized(&self) -> FieldResult<bool>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,13 @@ pub trait ServiceLocator: Send + Sync {
|
||||||
|
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
claims: Option<auth::Claims>,
|
claims: Option<auth::Claims>,
|
||||||
server: Arc<dyn ServiceLocator>,
|
locator: Arc<dyn ServiceLocator>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromAuth<Arc<dyn ServiceLocator>> for Context {
|
impl FromAuth<Arc<dyn ServiceLocator>> for Context {
|
||||||
fn build(server: Arc<dyn ServiceLocator>, bearer: Option<String>) -> Self {
|
fn build(locator: Arc<dyn ServiceLocator>, bearer: Option<String>) -> Self {
|
||||||
let claims = bearer.and_then(|token| validate_jwt(&token).ok());
|
let claims = bearer.and_then(|token| validate_jwt(&token).ok());
|
||||||
Self { claims, server }
|
Self { claims, locator }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,13 +46,17 @@ pub struct Query;
|
||||||
#[graphql_object(context = Context)]
|
#[graphql_object(context = Context)]
|
||||||
impl Query {
|
impl Query {
|
||||||
async fn workers(ctx: &Context) -> Vec<Worker> {
|
async fn workers(ctx: &Context) -> Vec<Worker> {
|
||||||
ctx.server.worker().list_workers().await
|
ctx.locator.worker().list_workers().await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn registration_token(ctx: &Context) -> FieldResult<String> {
|
async fn registration_token(ctx: &Context) -> FieldResult<String> {
|
||||||
let token = ctx.server.worker().read_registration_token().await?;
|
let token = ctx.locator.worker().read_registration_token().await?;
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn is_admin_initialized(ctx: &Context) -> FieldResult<bool> {
|
||||||
|
ctx.locator.auth().is_admin_initialized().await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -63,7 +67,7 @@ impl Mutation {
|
||||||
async fn reset_registration_token(ctx: &Context) -> FieldResult<String> {
|
async fn reset_registration_token(ctx: &Context) -> FieldResult<String> {
|
||||||
if let Some(claims) = &ctx.claims {
|
if let Some(claims) = &ctx.claims {
|
||||||
if claims.user_info().is_admin() {
|
if claims.user_info().is_admin() {
|
||||||
let reg_token = ctx.server.worker().reset_registration_token().await?;
|
let reg_token = ctx.locator.worker().reset_registration_token().await?;
|
||||||
return Ok(reg_token);
|
return Ok(reg_token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +83,7 @@ impl Mutation {
|
||||||
password1: String,
|
password1: String,
|
||||||
password2: String,
|
password2: String,
|
||||||
) -> FieldResult<RegisterResponse> {
|
) -> FieldResult<RegisterResponse> {
|
||||||
ctx.server
|
ctx.locator
|
||||||
.auth()
|
.auth()
|
||||||
.register(email, password1, password2)
|
.register(email, password1, password2)
|
||||||
.await
|
.await
|
||||||
|
|
@ -90,11 +94,11 @@ impl Mutation {
|
||||||
email: String,
|
email: String,
|
||||||
password: String,
|
password: String,
|
||||||
) -> FieldResult<TokenAuthResponse> {
|
) -> FieldResult<TokenAuthResponse> {
|
||||||
ctx.server.auth().token_auth(email, password).await
|
ctx.locator.auth().token_auth(email, password).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn verify_token(ctx: &Context, token: String) -> FieldResult<VerifyTokenResponse> {
|
async fn verify_token(ctx: &Context, token: String) -> FieldResult<VerifyTokenResponse> {
|
||||||
ctx.server.auth().verify_token(token).await
|
ctx.locator.auth().verify_token(token).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,11 @@ impl AuthenticationService for DbConn {
|
||||||
let resp = VerifyTokenResponse::new(claims);
|
let resp = VerifyTokenResponse::new(claims);
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn is_admin_initialized(&self) -> FieldResult<bool> {
|
||||||
|
let admin = self.list_admin_users().await?;
|
||||||
|
Ok(!admin.is_empty())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn password_hash(raw: &str) -> password_hash::Result<String> {
|
fn password_hash(raw: &str) -> password_hash::Result<String> {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use rusqlite::{params, OptionalExtension};
|
use rusqlite::{params, OptionalExtension, Row};
|
||||||
use rusqlite_migration::{AsyncMigrations, M};
|
use rusqlite_migration::{AsyncMigrations, M};
|
||||||
use tabby_common::path::tabby_root;
|
use tabby_common::path::tabby_root;
|
||||||
use tokio_rusqlite::Connection;
|
use tokio_rusqlite::Connection;
|
||||||
|
|
@ -47,6 +47,25 @@ pub struct User {
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
fn select(clause: &str) -> String {
|
||||||
|
r#"SELECT id, email, password_encrypted, is_admin, created_at, updated_at FROM users WHERE "#
|
||||||
|
.to_owned()
|
||||||
|
+ clause
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_row(row: &Row<'_>) -> std::result::Result<User, rusqlite::Error> {
|
||||||
|
Ok(User {
|
||||||
|
id: row.get(0)?,
|
||||||
|
email: row.get(1)?,
|
||||||
|
password_encrypted: row.get(2)?,
|
||||||
|
is_admin: row.get(3)?,
|
||||||
|
created_at: row.get(4)?,
|
||||||
|
updated_at: row.get(5)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn db_path() -> Result<PathBuf> {
|
async fn db_path() -> Result<PathBuf> {
|
||||||
let db_dir = tabby_root().join("ee");
|
let db_dir = tabby_root().join("ee");
|
||||||
tokio::fs::create_dir_all(db_dir.clone()).await?;
|
tokio::fs::create_dir_all(db_dir.clone()).await?;
|
||||||
|
|
@ -156,35 +175,51 @@ impl DbConn {
|
||||||
.conn
|
.conn
|
||||||
.call(move |c| {
|
.call(move |c| {
|
||||||
c.query_row(
|
c.query_row(
|
||||||
r#"SELECT id, email, password_encrypted, is_admin, created_at, updated_at FROM users WHERE email = ?"#,
|
User::select("email = ?").as_str(),
|
||||||
params![email],
|
params![email],
|
||||||
|row| {
|
User::from_row,
|
||||||
Ok(User {
|
)
|
||||||
id: row.get(0)?,
|
.optional()
|
||||||
email: row.get(1)?,
|
|
||||||
password_encrypted: row.get(2)?,
|
|
||||||
is_admin: row.get(3)?,
|
|
||||||
created_at: row.get(4)?,
|
|
||||||
updated_at: row.get(5)?,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
).optional()
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_admin_users(&self) -> Result<Vec<User>> {
|
||||||
|
let users = self
|
||||||
|
.conn
|
||||||
|
.call(move |c| {
|
||||||
|
let mut stmt = c.prepare(&User::select("is_admin"))?;
|
||||||
|
let user_iter = stmt.query_map([], User::from_row)?;
|
||||||
|
Ok(user_iter.filter_map(|x| x.ok()).collect::<Vec<_>>())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(users)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::schema::auth::AuthenticationService;
|
||||||
|
|
||||||
async fn new_in_memory() -> Result<DbConn> {
|
async fn new_in_memory() -> Result<DbConn> {
|
||||||
let conn = Connection::open_in_memory().await?;
|
let conn = Connection::open_in_memory().await?;
|
||||||
DbConn::init_db(conn).await
|
DbConn::init_db(conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_admin_user(conn: &DbConn) -> String {
|
||||||
|
let email = "test@example.com";
|
||||||
|
let passwd = "123456";
|
||||||
|
let is_admin = true;
|
||||||
|
conn.create_user(email.to_string(), passwd.to_string(), is_admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
email.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn migrations_test() {
|
async fn migrations_test() {
|
||||||
assert!(MIGRATIONS.validate().await.is_ok());
|
assert!(MIGRATIONS.validate().await.is_ok());
|
||||||
|
|
@ -212,14 +247,8 @@ mod tests {
|
||||||
async fn test_create_user() {
|
async fn test_create_user() {
|
||||||
let conn = new_in_memory().await.unwrap();
|
let conn = new_in_memory().await.unwrap();
|
||||||
|
|
||||||
let email = "test@example.com";
|
let email = create_admin_user(&conn).await;
|
||||||
let passwd = "123456";
|
let user = conn.get_user_by_email(&email).await.unwrap().unwrap();
|
||||||
let is_admin = true;
|
|
||||||
conn.create_user(email.to_string(), passwd.to_string(), is_admin)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let user = conn.get_user_by_email(email).await.unwrap().unwrap();
|
|
||||||
assert_eq!(user.id, 1);
|
assert_eq!(user.id, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,4 +261,13 @@ mod tests {
|
||||||
|
|
||||||
assert!(user.is_none());
|
assert!(user.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_is_admin_initialized() {
|
||||||
|
let conn = new_in_memory().await.unwrap();
|
||||||
|
|
||||||
|
assert!(!conn.is_admin_initialized().await.unwrap());
|
||||||
|
create_admin_user(&conn).await;
|
||||||
|
assert!(conn.is_admin_initialized().await.unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue