Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions notto-server/migrations/V1__init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
CREATE TABLE IF NOT EXISTS `user` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`stored_password_hash` TEXT NOT NULL,
`stored_recovery_hash` TEXT NOT NULL,
`encrypted_mek_password` BLOB NOT NULL,
`mek_password_nonce` BLOB NOT NULL,
`encrypted_mek_recovery` BLOB NOT NULL,
`mek_recovery_nonce` BLOB NOT NULL,
`salt_auth` VARCHAR(255) NOT NULL,
`salt_data` VARCHAR(255) NOT NULL,
`salt_recovery_auth` VARCHAR(255) NOT NULL,
`salt_recovery_data` VARCHAR(255) NOT NULL,
`salt_server_auth` VARCHAR(255) NOT NULL,
`salt_server_recovery` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_username` (`username`)
);

CREATE TABLE IF NOT EXISTS `user_token` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id_user` INT UNSIGNED NOT NULL,
`token` BLOB NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`id_user`) REFERENCES `user` (`id`) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS `note` (
`uuid` VARCHAR(36) NOT NULL,
`id_user` INT UNSIGNED NOT NULL,
`content` MEDIUMBLOB NOT NULL,
`nonce` BLOB NOT NULL,
`metadata` BLOB NOT NULL,
`metadata_nonce` BLOB NOT NULL,
`updated_at` BIGINT NOT NULL,
`deleted` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`uuid`, `id_user`),
FOREIGN KEY (`id_user`) REFERENCES `user` (`id`) ON DELETE CASCADE
);
17 changes: 16 additions & 1 deletion notto-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use crate::schema::User;

mod schema;

mod migrations;

/// Application error returned by all handlers.
/// Internal errors are logged server-side and return a generic 500 to the client.
pub struct AppError {
Expand Down Expand Up @@ -82,7 +84,7 @@ impl From<anyhow::Error> for AppError {
}

#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
dotenv().ok();
//Env var should be like mysql://user:pass%20word@localhost/database_name
let pool = Pool::new(
Expand All @@ -91,6 +93,17 @@ async fn main() {
.as_str(),
);

let mut conn = pool
.get_conn()
.await
.context("Failed to get DB connection for migrations")?;

migrations::run(&mut conn)
.await
.context("Failed to run database migrations")?;

drop(conn);

let app = Router::new()
.route("/notes", post(send_notes))
.route("/notes", get(select_notes))
Expand All @@ -112,6 +125,8 @@ async fn main() {
axum::serve(listener, app)
.await
.expect("Server error");

Ok(())
}

/// Verifies that `token` matches one of the stored tokens for `username`.
Expand Down
53 changes: 53 additions & 0 deletions notto-server/src/migrations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use anyhow::{Context, Result};
use mysql_async::{Conn, params, prelude::Queryable};

/// Each migration is a (version, sql) pair. Version must be monotonically increasing.
/// Append new entries here to add future migrations; never edit existing ones.
static MIGRATIONS: &[(u32, &str)] = &[
(1, include_str!("../migrations/V1__init.sql")),
];

/// Creates the tracking table if absent, then runs every migration whose version
/// is not yet recorded, in order.
pub async fn run(conn: &mut Conn) -> Result<()> {
conn.query_drop(
"CREATE TABLE IF NOT EXISTS `schema_migrations` (
`version` INT UNSIGNED NOT NULL,
`applied_at` BIGINT NOT NULL,
PRIMARY KEY (`version`)
)",
)
.await
.context("Failed to create schema_migrations table")?;

let applied: Vec<u32> = conn
.query("SELECT version FROM schema_migrations ORDER BY version")
.await
.context("Failed to query applied migrations")?;

for (version, sql) in MIGRATIONS {
if applied.contains(version) {
continue;
}

for statement in sql.split(';').map(str::trim).filter(|s| !s.is_empty()) {
conn.query_drop(statement)
.await
.with_context(|| format!("Migration V{version} failed on statement: {statement}"))?;
}

conn.exec_drop(
"INSERT INTO schema_migrations (version, applied_at) VALUES (:version, :applied_at)",
params! {
"version" => version,
"applied_at" => chrono::Local::now().to_utc().timestamp(),
},
)
.await
.with_context(|| format!("Failed to record migration V{version}"))?;

println!("Applied migration V{version}");
}

Ok(())
}