diff --git a/.env b/.env index b4d83b7..2b8d56c 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -DATABASE_URL=data/db.sqlite3 +DATABASE_URL=data/otter.sqlite3 diff --git a/README.md b/README.md index 3dc5fa6..213d16b 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ The (initial) goal here is only to support JSON. Other formats *might* be added on later, no guarantees. * Authentication API - - [ ] Login / Verify Login - - [ ] Logout + - [x] Login / Verify Login + - [x] Logout * Directory API - [ ] Retrieve Top Tags - [ ] Retrieve Podcasts for Tag diff --git a/diesel.toml b/diesel.toml index bb1d1f7..f348d3c 100644 --- a/diesel.toml +++ b/diesel.toml @@ -4,6 +4,7 @@ [print_schema] file = "src/db/schema.rs" custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] +sqlite_integer_primary_key_is_bigint = true [migrations_directory] dir = "migrations" diff --git a/migrations/2025-02-23-095541_auth/up.sql b/migrations/2025-02-23-095541_auth/up.sql index a081f00..40e99a7 100644 --- a/migrations/2025-02-23-095541_auth/up.sql +++ b/migrations/2025-02-23-095541_auth/up.sql @@ -1,5 +1,5 @@ create table users ( - id bigint primary key not null, + id integer primary key not null, username text unique not null, password_hash text not null ); diff --git a/src/cli/db.rs b/src/cli/db.rs new file mode 100644 index 0000000..23c5122 --- /dev/null +++ b/src/cli/db.rs @@ -0,0 +1,49 @@ +use clap::Subcommand; + +use crate::{db::DbResult, ErrorExt}; + +/// Tools to view and manage the database. +#[derive(Subcommand)] +pub enum DbCommand { + #[command(subcommand)] + Add(AddCommand), +} + +/// Insert a new entity into the database +#[derive(Subcommand)] +pub enum AddCommand { + User { username: String, password: String }, +} + +impl DbCommand { + pub fn run(&self, cli: &super::Cli) -> u8 { + match self { + DbCommand::Add(cmd) => cmd.run(cli), + } + } +} + +impl AddCommand { + pub fn run(&self, cli: &super::Cli) -> u8 { + match self.run_err(cli) { + Ok(()) => 0, + Err(err) => { + eprintln!("An error occured: {}", err.stack()); + + 1 + } + } + } + + pub fn run_err(&self, cli: &super::Cli) -> DbResult<()> { + let pool = crate::db::initialize_db(cli.data_dir.join(crate::DB_FILENAME), false)?; + + match self { + Self::User { username, password } => { + crate::db::NewUser::new(username.clone(), password.clone()).insert(&pool)?; + } + } + + Ok(()) + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 2efff8b..d2beb90 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,9 +1,12 @@ +mod db; mod serve; use std::path::PathBuf; use clap::{Parser, Subcommand}; +/// Otter is a lightweight implementation of the Gpodder API, designed to be used for small +/// personal deployments. #[derive(Parser)] pub struct Cli { #[arg( @@ -21,12 +24,15 @@ pub struct Cli { #[derive(Subcommand)] pub enum Command { Serve(serve::ServeCommand), + #[command(subcommand)] + Db(db::DbCommand), } impl Cli { pub fn run(&self) -> u8 { match &self.cmd { Command::Serve(cmd) => cmd.run(self), + Command::Db(cmd) => cmd.run(self), } } } diff --git a/src/cli/serve.rs b/src/cli/serve.rs index 8d7fde4..48a6c5b 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -2,8 +2,7 @@ use clap::Args; use crate::{db, server}; -const DB_FILENAME: &str = "otter.sqlite3"; - +/// Run the Otter web server #[derive(Args)] pub struct ServeCommand { #[arg( @@ -31,7 +30,7 @@ impl ServeCommand { tracing::info!("Initializing database and running migrations"); - let pool = db::initialize_db(cli.data_dir.join(DB_FILENAME), true).unwrap(); + let pool = db::initialize_db(cli.data_dir.join(crate::DB_FILENAME), true).unwrap(); let ctx = server::Context { pool }; let app = server::app().with_state(ctx); diff --git a/src/db/models/user.rs b/src/db/models/user.rs index f9c9bb9..f51226f 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -1,5 +1,5 @@ use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; -use diesel::prelude::*; +use diesel::{prelude::*, sqlite::Sqlite}; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; diff --git a/src/main.rs b/src/main.rs index 0d12122..b6136c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,27 @@ mod server; use clap::Parser; -use std::process::ExitCode; +use std::{fmt::Write, process::ExitCode}; + +const DB_FILENAME: &str = "otter.sqlite3"; + +pub trait ErrorExt: std::error::Error { + /// Return the full chain of error messages + fn stack(&self) -> String { + let mut msg = format!("{}", self); + let mut err = self.source(); + + while let Some(src) = err { + write!(msg, " - {}", src).unwrap(); + + err = src.source(); + } + + msg + } +} + +impl ErrorExt for E {} fn main() -> ExitCode { let args = cli::Cli::parse(); diff --git a/src/server/error.rs b/src/server/error.rs index 4e84dc9..5d7aeb4 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -1,8 +1,8 @@ -use std::fmt::{self, Write}; +use std::fmt; use axum::{http::StatusCode, response::IntoResponse}; -use crate::db; +use crate::{db, ErrorExt}; pub type AppResult = Result; @@ -40,24 +40,6 @@ impl std::error::Error for AppError { } } -pub trait ErrorExt: std::error::Error { - /// Return the full chain of error messages - fn stack(&self) -> String { - let mut msg = format!("{}", self); - let mut err = self.source(); - - while let Some(src) = err { - write!(msg, " - {}", src).unwrap(); - - err = src.source(); - } - - msg - } -} - -impl ErrorExt for E {} - impl From for AppError { fn from(value: db::DbError) -> Self { Self::Db(value)