diff --git a/.envrc b/.envrc index 5e9005b..253667b 100644 --- a/.envrc +++ b/.envrc @@ -2,3 +2,7 @@ if ! has nix_direnv_version || ! nix_direnv_version 2.1.1; then source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.1.1/direnvrc" "sha256-b6qJ4r34rbE23yWjMqbmu3ia2z4b2wIlZUksBke/ol0=" fi use flake + +export REGALADE_JWT_SECRET=B50D8FB3A6DC3073259062EFFD03A283416A81ED3183C44DEE179AFF76C00B58 +export REGALADE_DATABASE_URL=postgres://traxys/regalade?host=/var/run/postgresql +export DATABASE_URL=$REGALADE_DATABASE_URL diff --git a/Cargo.toml b/Cargo.toml index 8181c8f..868bc97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ authors = ["traxys "] edition = "2021" [workspace] -members = [".", "api", "app"] +members = [".", "api", "app", "migration"] [dependencies] anyhow = "1.0.71" @@ -18,5 +18,11 @@ tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } tracing = "0.1.37" tracing-subscriber = "0.3.17" api = { path = "./api" } +migration = { path = "./migration" } thiserror = "1.0.40" tower-http = { version = "0.4.0", features = ["cors", "fs"] } +sha2 = "0.10" + +[dependencies.sea-orm] +version = "0.11" +features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] diff --git a/flake.nix b/flake.nix index 67b3f15..a49df69 100644 --- a/flake.nix +++ b/flake.nix @@ -26,7 +26,12 @@ }; in { devShell = pkgs.mkShell { - nativeBuildInputs = [rust pkgs.trunk pkgs.httpie]; + nativeBuildInputs = [ + rust + pkgs.trunk + pkgs.httpie + pkgs.sea-orm-cli + ]; RUST_PATH = "${rust}"; RUST_DOC_PATH = "${rust}/share/doc/rust/html/std/index.html"; }; diff --git a/migration/Cargo.toml b/migration/Cargo.toml new file mode 100644 index 0000000..2a16573 --- /dev/null +++ b/migration/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +async-std = { version = "1", features = ["attributes", "tokio1"] } + +[dependencies.sea-orm-migration] +version = "0.11.0" +features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] diff --git a/migration/README.md b/migration/README.md new file mode 100644 index 0000000..b3ea53e --- /dev/null +++ b/migration/README.md @@ -0,0 +1,41 @@ +# Running Migrator CLI + +- Generate a new migration file + ```sh + cargo run -- migrate generate MIGRATION_NAME + ``` +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/migration/src/lib.rs b/migration/src/lib.rs new file mode 100644 index 0000000..c1fe52c --- /dev/null +++ b/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_orm_migration::prelude::*; + +mod m20220101_000001_account; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220101_000001_account::Migration)] + } +} diff --git a/migration/src/m20220101_000001_account.rs b/migration/src/m20220101_000001_account.rs new file mode 100644 index 0000000..a804786 --- /dev/null +++ b/migration/src/m20220101_000001_account.rs @@ -0,0 +1,35 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[derive(Iden)] +enum User { + Table, + Id, + Name, + Password, +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(User::Table) + .if_not_exists() + .col(ColumnDef::new(User::Id).uuid().not_null().primary_key()) + .col(ColumnDef::new(User::Name).string().not_null().unique_key()) + .col(ColumnDef::new(User::Password).binary_len(64).not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(User::Table).to_owned()) + .await + } +} diff --git a/migration/src/main.rs b/migration/src/main.rs new file mode 100644 index 0000000..c6b6e48 --- /dev/null +++ b/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[async_std::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/src/entity/mod.rs b/src/entity/mod.rs new file mode 100644 index 0000000..fb510ff --- /dev/null +++ b/src/entity/mod.rs @@ -0,0 +1,5 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 + +pub mod prelude; + +pub mod user; diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs new file mode 100644 index 0000000..7c8fdd7 --- /dev/null +++ b/src/entity/prelude.rs @@ -0,0 +1,3 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 + +pub use super::user::Entity as User; diff --git a/src/entity/user.rs b/src/entity/user.rs new file mode 100644 index 0000000..687cfbd --- /dev/null +++ b/src/entity/user.rs @@ -0,0 +1,19 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "user")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + #[sea_orm(unique)] + pub name: String, + #[sea_orm(column_type = "Binary(BlobSize::Blob(None))")] + pub password: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/main.rs b/src/main.rs index 6b2e6db..382c490 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,12 @@ use axum::Router; use base64::{engine::general_purpose, Engine}; use config::{Config, ConfigError}; use jwt_simple::prelude::HS256Key; +use migration::{Migrator, MigratorTrait}; +use sea_orm::{ConnectOptions, Database, DatabaseConnection}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tower_http::services::ServeDir; +pub(crate) mod entity; mod routes; #[derive(Clone)] @@ -69,6 +72,8 @@ struct Settings { port: u16, api_allowed: Option, serve_app: Option, + database_url: String, + sqlx_logging: bool, } impl Settings { @@ -79,6 +84,7 @@ impl Settings { .set_default("port", "8085")? .set_default("api_allowed", None::)? .set_default("serve_app", None::)? + .set_default("sqlx_logging", false)? .build()?; cfg.try_deserialize() @@ -87,6 +93,7 @@ impl Settings { struct AppState { jwt_secret: Base64, + db: DatabaseConnection, } #[tokio::main] @@ -102,10 +109,16 @@ async fn main() -> anyhow::Result<()> { let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?; + let mut opt = ConnectOptions::new(config.database_url); + opt.sqlx_logging(config.sqlx_logging); + let state = Arc::new(AppState { jwt_secret: config.jwt_secret, + db: Database::connect(opt).await?, }); + Migrator::up(&state.db, None).await?; + let router = Router::new() .nest( "/api", @@ -118,6 +131,8 @@ async fn main() -> anyhow::Result<()> { Some(path) => router.fallback_service(ServeDir::new(path)), }; + tracing::info!("Listening on {addr}"); + Ok(axum::Server::bind(&addr) .serve(router.into_make_service()) .await?)