Skip to content

Commit

Permalink
Add admin user creation
Browse files Browse the repository at this point in the history
  • Loading branch information
raffomania committed Dec 26, 2023
1 parent 00071dc commit 6e32dc7
Show file tree
Hide file tree
Showing 15 changed files with 248 additions and 14 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2021"

[dependencies]
anyhow = { version = "1.0.76"}
argon2 = "0.5.2"
askama = { version = "0.12.1", features = ["with-axum"] }
askama_axum = "0.4.0"
axum = { version = "0.7.2", features = ["macros", "tracing"] }
Expand All @@ -19,6 +20,7 @@ tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.5.0", features = ["tracing", "trace"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
uuid = { version = "1.6.1", features = ["v4"] }

[build-dependencies]
railwind = "0.1.5"
Expand Down
5 changes: 4 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ start-database:
stop-database:
podman stop linkblocks_postgres

wipe-database: stop-database
podman rm linkblocks_postgres

migrate-database:
cargo run -- db migrate
cargo sqlx migrate run

test:
cargo test
Expand Down
4 changes: 3 additions & 1 deletion migrations/20231224122154_users.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
create table users (
id uuid primary key default gen_random_uuid() not null
id uuid primary key default gen_random_uuid() not null,
password_hash text not null,
username text not null
);
18 changes: 12 additions & 6 deletions src/app_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub enum AppError {

impl IntoResponse for AppError {
fn into_response(self) -> Response {
tracing::debug!("{self:?}");
tracing::error!("{self:?}");
match self {
AppError::Anyhow(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error"),
AppError::NotFound() => (StatusCode::NOT_FOUND, "Not Found"),
Expand All @@ -22,11 +22,17 @@ impl IntoResponse for AppError {
}
}

impl<Error> From<Error> for AppError
where
Error: Into<anyhow::Error>,
{
fn from(value: Error) -> Self {
impl From<anyhow::Error> for AppError {
fn from(value: anyhow::Error) -> Self {
Self::Anyhow(value.into())
}
}

impl From<sqlx::Error> for AppError {
fn from(value: sqlx::Error) -> Self {
match value {
sqlx::Error::RowNotFound => Self::NotFound(),
other => Self::Anyhow(other.into()),
}
}
}
35 changes: 33 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilte

use clap::{Args, Parser, Subcommand};

use crate::{db, server};
use crate::{db, schemas::users::CreateUser, server};

#[derive(Parser)]
#[clap(version)]
struct Cli {
#[command(subcommand)]
command: Command,
Expand All @@ -21,19 +22,36 @@ struct SharedConfig {
database_url: String,
}

#[derive(Subcommand)]
#[derive(Parser)]
enum Command {
/// Migrate the database, then start the server
Start {
#[clap(flatten)]
listen: ListenArgs,
#[clap(flatten)]
admin_credentials: AdminCredentials,
},
Db {
#[clap(subcommand)]
command: DbCommand,
},
}

#[derive(Args)]
#[group(multiple = true, requires_all = ["username", "password"])]
struct AdminCredentials {
#[clap(env = "ADMIN_USERNAME", long = "admin_username")]
/// Create an admin user if it does not exist yet.
username: Option<String>,
#[clap(
env = "ADMIN_PASSWORD",
long = "admin_password",
hide_env_values = true
)]
/// Password for the admin user.
password: Option<String>,
}

#[derive(Subcommand)]
enum DbCommand {
Migrate,
Expand Down Expand Up @@ -63,9 +81,22 @@ pub async fn run() -> Result<()> {
match cli.command {
Command::Start {
listen: listen_address,
admin_credentials,
} => {
let pool = db::pool(&cli.config.database_url).await?;

db::migrate(&pool).await?;

if let (Some(username), Some(password)) =
(admin_credentials.username, admin_credentials.password)
{
let mut tx = pool.begin().await?;
db::users::create_user_if_not_exists(&mut tx, CreateUser { password, username })
.await
.unwrap();
tx.commit().await?;
}

let app = server::app(pool);
server::start(listen_address, app).await
}
Expand Down
2 changes: 2 additions & 0 deletions src/db.rs → src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use sqlx::PgPool;

use crate::app_error::AppError;

pub mod users;

pub async fn migrate(pool: &PgPool) -> Result<()> {
tracing::info!("Migrating the database...");
sqlx::migrate!("./migrations").run(pool).await?;
Expand Down
79 changes: 79 additions & 0 deletions src/db/users.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use anyhow::anyhow;
use sqlx::prelude::FromRow;
use sqlx::{query, query_as, Postgres, Transaction};
use uuid::Uuid;

use crate::app_error::{AppError, Result};
use crate::schemas::users::CreateUser;

#[derive(FromRow, Debug)]
pub struct User {
pub id: Uuid,
pub username: String,
pub password_hash: String,
}

pub async fn insert(db: &mut Transaction<'_, Postgres>, create: CreateUser) -> Result<()> {
let hashed_password = hash_password(create.password)?;

query!(
r#"
insert into users
(password_hash, username)
values ($1, $2)"#,
hashed_password,
create.username
)
.execute(&mut **db)
.await?;

Ok(())
}

pub async fn by_username(db: &mut Transaction<'_, Postgres>, username: String) -> Result<User> {
let user = query_as!(
User,
r#"
select * from users
where username = $1
"#,
username
)
.fetch_one(&mut **db)
.await?;

Ok(user)
}

pub async fn create_user_if_not_exists(
tx: &mut Transaction<'_, Postgres>,
create: CreateUser,
) -> Result<()> {
let username = create.username.clone();
tracing::info!("Checking if admin user '{username}' exists...");
let user = by_username(tx, username).await;
match user {
Err(AppError::NotFound()) => {
tracing::info!("Creating admin user");
insert(tx, create).await?;
}
Ok(_) => {
tracing::info!("User already exists")
}
Err(other) => return Err(other),
}
Ok(())
}

fn hash_password(password: String) -> Result<String> {
let salt =
argon2::password_hash::SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);

let argon2 = argon2::Argon2::default();

Ok(
argon2::PasswordHasher::hash_password(&argon2, password.as_bytes(), &salt)
.map_err(|e| anyhow!("Failed to hash password: {e}"))?
.to_string(),
)
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ mod app_error;
pub mod cli;
mod db;
mod routes;
mod schemas;
pub mod server;
Loading

0 comments on commit 6e32dc7

Please sign in to comment.