diff --git a/musicus/.gitignore b/.gitignore similarity index 100% rename from musicus/.gitignore rename to .gitignore diff --git a/musicus/Cargo.toml b/Cargo.toml similarity index 100% rename from musicus/Cargo.toml rename to Cargo.toml diff --git a/README.md b/README.md index 68f4075..342cd95 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,63 @@ # Musicus -The classical music player and organizer. +This is a desktop app for Musicus. https://musicus.org -## Repository structure +## Hacking -The subdirectories contain toplevel components of the Musicus system. Currently -you will find: +### Building - * `musicus` – Musicus desktop app for Linux - * `musicus_server` – Musicus server +Musicus uses the [Meson build system](https://mesonbuild.com/). You can build +it using the following commands: + +``` +$ meson build +$ ninja -C build +``` + +Afterwards the resulting binary executable is under +`build/target/debug/musicus`. + +### Flatpak + +There is a Flatpak manifest file called `de.johrpan.musicus.json`. To build a +Flatpak you need the the latest Gnome SDK and the Freedesktop SDK with the Rust +extension. You can install those using the following commands: + +``` +$ flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo +$ flatpak remote-add --user --if-not-exists gnome-nightly https://nightly.gnome.org/gnome-nightly.flatpakrepo +$ flatpak install --user gnome-nightly org.gnome.Sdk org.gnome.Platform +$ flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//19.08 +``` + +Afterwards, the following commands will build, install and run the application: + +``` +$ rm -rf flatpak +$ flatpak-builder --user --install flatpak de.johrpan.musicus.json +$ flatpak run de.johrpan.musicus +``` + +### Special requirements + +This program uses [Diesel](https://diesel.rs) as its ORM. After installing +the Diesel command line utility, you will be able to create a new schema +migration using the following command: + +``` +$ diesel migration generate [change_description] +``` + +To update the `src/database/schema.rs` file, you should use the following +command: + +``` +$ diesel migration run --database-url test.sqlite +``` + +This file should never be edited manually. ## License diff --git a/musicus/build-aux/cargo.sh b/build-aux/cargo.sh similarity index 100% rename from musicus/build-aux/cargo.sh rename to build-aux/cargo.sh diff --git a/musicus/build-aux/postinstall.py b/build-aux/postinstall.py similarity index 100% rename from musicus/build-aux/postinstall.py rename to build-aux/postinstall.py diff --git a/musicus/data/de.johrpan.musicus.desktop.in b/data/de.johrpan.musicus.desktop.in similarity index 100% rename from musicus/data/de.johrpan.musicus.desktop.in rename to data/de.johrpan.musicus.desktop.in diff --git a/musicus/data/de.johrpan.musicus.gschema.xml b/data/de.johrpan.musicus.gschema.xml similarity index 100% rename from musicus/data/de.johrpan.musicus.gschema.xml rename to data/de.johrpan.musicus.gschema.xml diff --git a/musicus/data/icons/hicolor/scalable/apps/de.johrpan.musicus.svg b/data/icons/hicolor/scalable/apps/de.johrpan.musicus.svg similarity index 100% rename from musicus/data/icons/hicolor/scalable/apps/de.johrpan.musicus.svg rename to data/icons/hicolor/scalable/apps/de.johrpan.musicus.svg diff --git a/musicus/data/icons/hicolor/symbolic/apps/de.johrpan.musicus-symbolic.svg b/data/icons/hicolor/symbolic/apps/de.johrpan.musicus-symbolic.svg similarity index 100% rename from musicus/data/icons/hicolor/symbolic/apps/de.johrpan.musicus-symbolic.svg rename to data/icons/hicolor/symbolic/apps/de.johrpan.musicus-symbolic.svg diff --git a/musicus/data/meson.build b/data/meson.build similarity index 100% rename from musicus/data/meson.build rename to data/meson.build diff --git a/musicus/de.johrpan.musicus.json b/de.johrpan.musicus.json similarity index 100% rename from musicus/de.johrpan.musicus.json rename to de.johrpan.musicus.json diff --git a/musicus/diesel.toml b/diesel.toml similarity index 100% rename from musicus/diesel.toml rename to diesel.toml diff --git a/musicus/meson.build b/meson.build similarity index 100% rename from musicus/meson.build rename to meson.build diff --git a/musicus/migrations/2020-09-27-201047_initial_schema/down.sql b/migrations/2020-09-27-201047_initial_schema/down.sql similarity index 100% rename from musicus/migrations/2020-09-27-201047_initial_schema/down.sql rename to migrations/2020-09-27-201047_initial_schema/down.sql diff --git a/musicus/migrations/2020-09-27-201047_initial_schema/up.sql b/migrations/2020-09-27-201047_initial_schema/up.sql similarity index 100% rename from musicus/migrations/2020-09-27-201047_initial_schema/up.sql rename to migrations/2020-09-27-201047_initial_schema/up.sql diff --git a/musicus/README.md b/musicus/README.md deleted file mode 100644 index 342cd95..0000000 --- a/musicus/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Musicus - -This is a desktop app for Musicus. - -https://musicus.org - -## Hacking - -### Building - -Musicus uses the [Meson build system](https://mesonbuild.com/). You can build -it using the following commands: - -``` -$ meson build -$ ninja -C build -``` - -Afterwards the resulting binary executable is under -`build/target/debug/musicus`. - -### Flatpak - -There is a Flatpak manifest file called `de.johrpan.musicus.json`. To build a -Flatpak you need the the latest Gnome SDK and the Freedesktop SDK with the Rust -extension. You can install those using the following commands: - -``` -$ flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo -$ flatpak remote-add --user --if-not-exists gnome-nightly https://nightly.gnome.org/gnome-nightly.flatpakrepo -$ flatpak install --user gnome-nightly org.gnome.Sdk org.gnome.Platform -$ flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//19.08 -``` - -Afterwards, the following commands will build, install and run the application: - -``` -$ rm -rf flatpak -$ flatpak-builder --user --install flatpak de.johrpan.musicus.json -$ flatpak run de.johrpan.musicus -``` - -### Special requirements - -This program uses [Diesel](https://diesel.rs) as its ORM. After installing -the Diesel command line utility, you will be able to create a new schema -migration using the following command: - -``` -$ diesel migration generate [change_description] -``` - -To update the `src/database/schema.rs` file, you should use the following -command: - -``` -$ diesel migration run --database-url test.sqlite -``` - -This file should never be edited manually. - -## License - -Musicus is free and open source software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your option) -any later version. - -Musicus is distributed in the hope that it will be useful, but WITHOUT ANY -WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -details. - -You should have received a copy of the GNU Affero General Public License along -with this program. If not, see https://www.gnu.org/licenses/. \ No newline at end of file diff --git a/musicus_server/.gitignore b/musicus_server/.gitignore deleted file mode 100644 index 93fdb52..0000000 --- a/musicus_server/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/.env -/Cargo.lock -/target \ No newline at end of file diff --git a/musicus_server/Cargo.toml b/musicus_server/Cargo.toml deleted file mode 100644 index a3e7312..0000000 --- a/musicus_server/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "musicus_server" -version = "0.1.0" -edition = "2018" - -[dependencies] -actix-web = "3.2.0" -actix-web-httpauth = "0.5.0" -anyhow = "1.0.34" -derive_more = "0.99.11" -diesel = { version = "1.4.4", features = ["postgres", "r2d2"] } -diesel_migrations = "1.4.0" -dotenv = "0.15.0" -env_logger = "0.8.1" -jsonwebtoken = "7.2.0" -r2d2 = "0.8.9" -rand = "0.7.3" -serde = { version = "1.0.117", features = ["derive"] } -serde_json = "1.0.59" -sodiumoxide = "0.2.6" diff --git a/musicus_server/README.md b/musicus_server/README.md deleted file mode 100644 index c8190f8..0000000 --- a/musicus_server/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Musicus Server - -This is a server for hosting metadata on classical music. - -## Running - -The Musicus server should reside behind a reverse proxy (e.g. Nginx) that is -set up to only use TLS encrypted connections. You will need a running -[PostgreSQL](https://www.postgresql.org/) service. To set up the database (and -migrate to future versions) use the Diesel command line utility from within -the source code repository. This utility and the Musicus server itself use the -environment variable `MUSICUS_DATABASE_URL` to find the database. A nice way to -set it up is to use a file called `.env` within the toplevel directory of the -repository. - -```bash -# Install the Diesel command line utility: -cargo install diesel_cli --no-default-features --features postgres - -# Configure the database URL (replace username and table): -echo "MUSICUS_DATABASE_URL=\"postgres://username@localhost/table\"" >> .env - -# Run migrations: -~/.cargo/bin/diesel migration run - -# Set a secret that will be used to sign access tokens: -echo "MUSICUS_SECRET=\"$(openssl rand -base64 64)\"" >> .env -``` - -## Hacking - -The Musicus server is written in [Rust](https://www.rust-lang.org) using the -[Actix Web](https://actix.rs/) framework for serving requests and -[Diesel](https://diesel.rs/) for database access. The linked websites should -provide you with the necessary information to get started. - -## License - -Musicus is free and open source software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by the -Free Software Foundation, either version 3 of the License, or (at your option) -any later version. - -Musicus is distributed in the hope that it will be useful, but WITHOUT ANY -WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -details. - -You should have received a copy of the GNU Affero General Public License along -with this program. If not, see https://www.gnu.org/licenses/. \ No newline at end of file diff --git a/musicus_server/diesel.toml b/musicus_server/diesel.toml deleted file mode 100644 index d3e7db2..0000000 --- a/musicus_server/diesel.toml +++ /dev/null @@ -1,2 +0,0 @@ -[print_schema] -file = "src/database/schema.rs" \ No newline at end of file diff --git a/musicus_server/migrations/00000000000000_diesel_initial_setup/down.sql b/musicus_server/migrations/00000000000000_diesel_initial_setup/down.sql deleted file mode 100644 index a9f5260..0000000 --- a/musicus_server/migrations/00000000000000_diesel_initial_setup/down.sql +++ /dev/null @@ -1,6 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - -DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); -DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/musicus_server/migrations/00000000000000_diesel_initial_setup/up.sql b/musicus_server/migrations/00000000000000_diesel_initial_setup/up.sql deleted file mode 100644 index d68895b..0000000 --- a/musicus_server/migrations/00000000000000_diesel_initial_setup/up.sql +++ /dev/null @@ -1,36 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - - - - --- Sets up a trigger for the given table to automatically set a column called --- `updated_at` whenever the row is modified (unless `updated_at` was included --- in the modified columns) --- --- # Example --- --- ```sql --- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); --- --- SELECT diesel_manage_updated_at('users'); --- ``` -CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ -BEGIN - EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s - FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); -END; -$$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ -BEGIN - IF ( - NEW IS DISTINCT FROM OLD AND - NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at - ) THEN - NEW.updated_at := current_timestamp; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; diff --git a/musicus_server/migrations/2020-11-09-153819_initial_schema/down.sql b/musicus_server/migrations/2020-11-09-153819_initial_schema/down.sql deleted file mode 100644 index 8c6540d..0000000 --- a/musicus_server/migrations/2020-11-09-153819_initial_schema/down.sql +++ /dev/null @@ -1,19 +0,0 @@ -DROP TABLE performances; - -DROP TABLE recordings; - -DROP TABLE ensembles; - -DROP TABLE work_sections; - -DROP TABLE work_parts; - -DROP TABLE instrumentations; - -DROP TABLE works; - -DROP TABLE instruments; - -DROP TABLE persons; - -DROP TABLE users; \ No newline at end of file diff --git a/musicus_server/migrations/2020-11-09-153819_initial_schema/up.sql b/musicus_server/migrations/2020-11-09-153819_initial_schema/up.sql deleted file mode 100644 index 3abb11b..0000000 --- a/musicus_server/migrations/2020-11-09-153819_initial_schema/up.sql +++ /dev/null @@ -1,70 +0,0 @@ -CREATE TABLE users ( - username TEXT NOT NULL PRIMARY KEY, - password_hash TEXT NOT NULL, - email TEXT, - is_admin BOOLEAN NOT NULL DEFAULT FALSE, - is_editor BOOLEAN NOT NULL DEFAULT FALSE, - is_banned BOOLEAN NOT NULL DEFAULT FALSE -); - -CREATE TABLE persons ( - id TEXT NOT NULL PRIMARY KEY, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, - created_by TEXT NOT NULL REFERENCES users(username) -); - -CREATE TABLE instruments ( - id TEXT NOT NULL PRIMARY KEY, - name TEXT NOT NULL, - created_by TEXT NOT NULL REFERENCES users(username) -); - -CREATE TABLE works ( - id TEXT NOT NULL PRIMARY KEY, - composer TEXT NOT NULL REFERENCES persons(id), - title TEXT NOT NULL, - created_by TEXT NOT NULL REFERENCES users(username) -); - -CREATE TABLE instrumentations ( - id BIGINT NOT NULL PRIMARY KEY, - work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, - instrument TEXT NOT NULL REFERENCES instruments(id) ON DELETE CASCADE -); - -CREATE TABLE work_parts ( - id BIGINT NOT NULL PRIMARY KEY, - work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, - part_index BIGINT NOT NULL, - title TEXT NOT NULL, - composer TEXT REFERENCES persons(id) -); - -CREATE TABLE work_sections ( - id BIGINT NOT NULL PRIMARY KEY, - work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, - title TEXT NOT NULL, - before_index BIGINT NOT NULL -); - -CREATE TABLE ensembles ( - id TEXT NOT NULL PRIMARY KEY, - name TEXT NOT NULL, - created_by TEXT NOT NULL REFERENCES users(username) -); - -CREATE TABLE recordings ( - id TEXT NOT NULL PRIMARY KEY, - work TEXT NOT NULL REFERENCES works(id), - comment TEXT NOT NULL, - created_by TEXT NOT NULL REFERENCES users(username) -); - -CREATE TABLE performances ( - id BIGINT NOT NULL PRIMARY KEY, - recording TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE, - person TEXT REFERENCES persons(id), - ensemble TEXT REFERENCES ensembles(id), - role TEXT REFERENCES instruments(id) -); \ No newline at end of file diff --git a/musicus_server/src/database/ensembles.rs b/musicus_server/src/database/ensembles.rs deleted file mode 100644 index 32eb94c..0000000 --- a/musicus_server/src/database/ensembles.rs +++ /dev/null @@ -1,99 +0,0 @@ -use super::schema::ensembles; -use super::{DbConn, User}; -use crate::error::ServerError; -use anyhow::{Error, Result}; -use diesel::prelude::*; -use serde::{Deserialize, Serialize}; - -/// A ensemble as represented within the API. -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Ensemble { - pub id: String, - pub name: String, -} - -/// A ensemble as represented in the database. -#[derive(Insertable, Queryable, AsChangeset, Debug, Clone)] -#[table_name = "ensembles"] -struct EnsembleRow { - pub id: String, - pub name: String, - pub created_by: String, -} - -impl From for Ensemble { - fn from(row: EnsembleRow) -> Ensemble { - Ensemble { - id: row.id, - name: row.name, - } - } -} - -/// Update an existing ensemble or insert a new one. This will only work, if the provided user is -/// allowed to do that. -pub fn update_ensemble(conn: &DbConn, ensemble: &Ensemble, user: &User) -> Result<()> { - let old_row = get_ensemble_row(conn, &ensemble.id)?; - - let allowed = match old_row { - Some(row) => user.may_edit(&row.created_by), - None => user.may_create(), - }; - - if allowed { - let new_row = EnsembleRow { - id: ensemble.id.clone(), - name: ensemble.name.clone(), - created_by: user.username.clone(), - }; - - diesel::insert_into(ensembles::table) - .values(&new_row) - .on_conflict(ensembles::id) - .do_update() - .set(&new_row) - .execute(conn)?; - - Ok(()) - } else { - Err(Error::new(ServerError::Forbidden)) - } -} - -/// Get an existing ensemble. -pub fn get_ensemble(conn: &DbConn, id: &str) -> Result> { - let row = get_ensemble_row(conn, id)?; - let ensemble = row.map(|row| row.into()); - - Ok(ensemble) -} - -/// Delete an existing ensemble. This will only work if the provided user is allowed to do that. -pub fn delete_ensemble(conn: &DbConn, id: &str, user: &User) -> Result<()> { - if user.may_delete() { - diesel::delete(ensembles::table.filter(ensembles::id.eq(id))).execute(conn)?; - Ok(()) - } else { - Err(Error::new(ServerError::Forbidden)) - } -} - -/// Get all existing ensembles. -pub fn get_ensembles(conn: &DbConn) -> Result> { - let rows = ensembles::table.load::(conn)?; - let ensembles: Vec = rows.into_iter().map(|row| row.into()).collect(); - - Ok(ensembles) -} - -/// Get a ensemble row if it exists. -fn get_ensemble_row(conn: &DbConn, id: &str) -> Result> { - let row = ensembles::table - .filter(ensembles::id.eq(id)) - .load::(conn)? - .into_iter() - .next(); - - Ok(row) -} diff --git a/musicus_server/src/database/instruments.rs b/musicus_server/src/database/instruments.rs deleted file mode 100644 index 3b4a7fe..0000000 --- a/musicus_server/src/database/instruments.rs +++ /dev/null @@ -1,99 +0,0 @@ -use super::schema::instruments; -use super::{DbConn, User}; -use crate::error::ServerError; -use anyhow::{Error, Result}; -use diesel::prelude::*; -use serde::{Deserialize, Serialize}; - -/// A instrument as represented within the API. -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Instrument { - pub id: String, - pub name: String, -} - -/// A instrument as represented in the database. -#[derive(Insertable, Queryable, AsChangeset, Debug, Clone)] -#[table_name = "instruments"] -struct InstrumentRow { - pub id: String, - pub name: String, - pub created_by: String, -} - -impl From for Instrument { - fn from(row: InstrumentRow) -> Instrument { - Instrument { - id: row.id, - name: row.name, - } - } -} - -/// Update an existing instrument or insert a new one. This will only work, if the provided user is -/// allowed to do that. -pub fn update_instrument(conn: &DbConn, instrument: &Instrument, user: &User) -> Result<()> { - let old_row = get_instrument_row(conn, &instrument.id)?; - - let allowed = match old_row { - Some(row) => user.may_edit(&row.created_by), - None => user.may_create(), - }; - - if allowed { - let new_row = InstrumentRow { - id: instrument.id.clone(), - name: instrument.name.clone(), - created_by: user.username.clone(), - }; - - diesel::insert_into(instruments::table) - .values(&new_row) - .on_conflict(instruments::id) - .do_update() - .set(&new_row) - .execute(conn)?; - - Ok(()) - } else { - Err(Error::new(ServerError::Forbidden)) - } -} - -/// Get an existing instrument. -pub fn get_instrument(conn: &DbConn, id: &str) -> Result> { - let row = get_instrument_row(conn, id)?; - let instrument = row.map(|row| row.into()); - - Ok(instrument) -} - -/// Delete an existing instrument. This will only work if the provided user is allowed to do that. -pub fn delete_instrument(conn: &DbConn, id: &str, user: &User) -> Result<()> { - if user.may_delete() { - diesel::delete(instruments::table.filter(instruments::id.eq(id))).execute(conn)?; - Ok(()) - } else { - Err(Error::new(ServerError::Forbidden)) - } -} - -/// Get all existing instruments. -pub fn get_instruments(conn: &DbConn) -> Result> { - let rows = instruments::table.load::(conn)?; - let instruments: Vec = rows.into_iter().map(|row| row.into()).collect(); - - Ok(instruments) -} - -/// Get a instrument row if it exists. -fn get_instrument_row(conn: &DbConn, id: &str) -> Result> { - let row = instruments::table - .filter(instruments::id.eq(id)) - .load::(conn)? - .into_iter() - .next(); - - Ok(row) -} diff --git a/musicus_server/src/database/mod.rs b/musicus_server/src/database/mod.rs deleted file mode 100644 index 53b38f3..0000000 --- a/musicus_server/src/database/mod.rs +++ /dev/null @@ -1,46 +0,0 @@ -use anyhow::Result; -use diesel::r2d2; -use diesel::PgConnection; - -pub mod ensembles; -pub use ensembles::*; - -pub mod instruments; -pub use instruments::*; - -pub mod persons; -pub use persons::*; - -pub mod recordings; -pub use recordings::*; - -pub mod users; -pub use users::*; - -pub mod works; -pub use works::*; - -mod schema; - -// This makes the SQL migration scripts accessible from the code. -embed_migrations!(); - -/// A pool of connections to the database. -pub type DbPool = r2d2::Pool>; - -/// One database connection from the connection pool. -pub type DbConn = r2d2::PooledConnection>; - -/// Create a connection pool for a database. This will look for the database URL in the -/// "MUSICUS_DATABASE_URL" environment variable and fail, if that is not set. -pub fn connect() -> Result { - let url = std::env::var("MUSICUS_DATABASE_URL")?; - let manager = r2d2::ConnectionManager::::new(url); - let pool = r2d2::Pool::new(manager)?; - - // Run embedded migrations. - let conn = pool.get()?; - embedded_migrations::run(&conn)?; - - Ok(pool) -} diff --git a/musicus_server/src/database/persons.rs b/musicus_server/src/database/persons.rs deleted file mode 100644 index df2c704..0000000 --- a/musicus_server/src/database/persons.rs +++ /dev/null @@ -1,103 +0,0 @@ -use super::schema::persons; -use super::{DbConn, User}; -use crate::error::ServerError; -use anyhow::{Error, Result}; -use diesel::prelude::*; -use serde::{Deserialize, Serialize}; - -/// A person as represented within the API. -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Person { - pub id: String, - pub first_name: String, - pub last_name: String, -} - -/// A person as represented in the database. -#[derive(Insertable, Queryable, AsChangeset, Debug, Clone)] -#[table_name = "persons"] -struct PersonRow { - pub id: String, - pub first_name: String, - pub last_name: String, - pub created_by: String, -} - -impl From for Person { - fn from(row: PersonRow) -> Person { - Person { - id: row.id, - first_name: row.first_name, - last_name: row.last_name, - } - } -} - -/// Update an existing person or insert a new one. This will only work, if the provided user is -/// allowed to do that. -pub fn update_person(conn: &DbConn, person: &Person, user: &User) -> Result<()> { - let old_row = get_person_row(conn, &person.id)?; - - let allowed = match old_row { - Some(row) => user.may_edit(&row.created_by), - None => user.may_create(), - }; - - if allowed { - let new_row = PersonRow { - id: person.id.clone(), - first_name: person.first_name.clone(), - last_name: person.last_name.clone(), - created_by: user.username.clone(), - }; - - diesel::insert_into(persons::table) - .values(&new_row) - .on_conflict(persons::id) - .do_update() - .set(&new_row) - .execute(conn)?; - - Ok(()) - } else { - Err(Error::new(ServerError::Forbidden)) - } -} - -/// Get an existing person. -pub fn get_person(conn: &DbConn, id: &str) -> Result> { - let row = get_person_row(conn, id)?; - let person = row.map(|row| row.into()); - - Ok(person) -} - -/// Delete an existing person. This will only work if the provided user is allowed to do that. -pub fn delete_person(conn: &DbConn, id: &str, user: &User) -> Result<()> { - if user.may_delete() { - diesel::delete(persons::table.filter(persons::id.eq(id))).execute(conn)?; - Ok(()) - } else { - Err(Error::new(ServerError::Forbidden)) - } -} - -/// Get all existing persons. -pub fn get_persons(conn: &DbConn) -> Result> { - let rows = persons::table.load::(conn)?; - let persons: Vec = rows.into_iter().map(|row| row.into()).collect(); - - Ok(persons) -} - -/// Get a person row if it exists. -fn get_person_row(conn: &DbConn, id: &str) -> Result> { - let row = persons::table - .filter(persons::id.eq(id)) - .load::(conn)? - .into_iter() - .next(); - - Ok(row) -} diff --git a/musicus_server/src/database/recordings.rs b/musicus_server/src/database/recordings.rs deleted file mode 100644 index 1452746..0000000 --- a/musicus_server/src/database/recordings.rs +++ /dev/null @@ -1,255 +0,0 @@ -use super::schema::{ensembles, performances, persons, recordings}; -use super::{get_ensemble, get_instrument, get_person, get_work}; -use super::{update_ensemble, update_instrument, update_person, update_work}; -use super::{DbConn, Ensemble, Instrument, Person, User, Work}; -use crate::error::ServerError; -use anyhow::{anyhow, Error, Result}; -use diesel::prelude::*; -use serde::{Deserialize, Serialize}; - -/// A specific recording of a work. -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Recording { - pub id: String, - pub work: Work, - pub comment: String, - pub performances: Vec, -} - -/// How a person or ensemble was involved in a recording. -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Performance { - pub person: Option, - pub ensemble: Option, - pub role: Option, -} - -/// Row data for a recording. -#[derive(Insertable, Queryable, Debug, Clone)] -#[table_name = "recordings"] -struct RecordingRow { - pub id: String, - pub work: String, - pub comment: String, - pub created_by: String, -} - -/// Row data for a performance. -#[derive(Insertable, Queryable, Debug, Clone)] -#[table_name = "performances"] -struct PerformanceRow { - pub id: i64, - pub recording: String, - pub person: Option, - pub ensemble: Option, - pub role: Option, -} - -/// Update an existing recording or insert a new one. This will only work, if the provided user is -/// allowed to do that. -pub fn update_recording(conn: &DbConn, recording: &Recording, user: &User) -> Result<()> { - conn.transaction::<(), Error, _>(|| { - let old_row = get_recording_row(conn, &recording.id)?; - - let allowed = match old_row { - Some(row) => user.may_edit(&row.created_by), - None => user.may_create(), - }; - - if allowed { - let id = &recording.id; - - // This will also delete the old performances. - diesel::delete(recordings::table) - .filter(recordings::id.eq(id)) - .execute(conn)?; - - // Add associated items, if they don't already exist. - - if get_work(conn, &recording.work.id)?.is_none() { - update_work(conn, &recording.work, &user)?; - } - - for performance in &recording.performances { - if let Some(person) = &performance.person { - if get_person(conn, &person.id)?.is_none() { - update_person(conn, person, &user)?; - } - } - - if let Some(ensemble) = &performance.ensemble { - if get_ensemble(conn, &ensemble.id)?.is_none() { - update_ensemble(conn, ensemble, &user)?; - } - } - - if let Some(role) = &performance.role { - if get_instrument(conn, &role.id)?.is_none() { - update_instrument(conn, role, &user)?; - } - } - } - - // Add the actual recording. - - let row = RecordingRow { - id: id.clone(), - work: recording.work.id.clone(), - comment: recording.comment.clone(), - created_by: user.username.clone(), - }; - - diesel::insert_into(recordings::table) - .values(row) - .execute(conn)?; - - for performance in &recording.performances { - diesel::insert_into(performances::table) - .values(PerformanceRow { - id: rand::random(), - recording: id.clone(), - person: performance.person.as_ref().map(|person| person.id.clone()), - ensemble: performance - .ensemble - .as_ref() - .map(|ensemble| ensemble.id.clone()), - role: performance.role.as_ref().map(|role| role.id.clone()), - }) - .execute(conn)?; - } - - Ok(()) - } else { - Err(Error::new(ServerError::Forbidden)) - } - })?; - - Ok(()) -} - -/// Get an existing recording and all available information from related tables. -pub fn get_recording(conn: &DbConn, id: &str) -> Result> { - let recording = match get_recording_row(conn, id)? { - Some(row) => Some(get_description_for_recording_row(conn, &row)?), - None => None, - }; - - Ok(recording) -} - -/// Get all available information on all recordings where a person is performing. -pub fn get_recordings_for_person(conn: &DbConn, person_id: &str) -> Result> { - let mut recordings: Vec = Vec::new(); - - let rows = recordings::table - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(persons::table.on(persons::id.nullable().eq(performances::person))) - .filter(persons::id.eq(person_id)) - .select(recordings::table::all_columns()) - .load::(conn)?; - - for row in rows { - recordings.push(get_description_for_recording_row(conn, &row)?); - } - - Ok(recordings) -} - -/// Get all available information on all recordings where an ensemble is performing. -pub fn get_recordings_for_ensemble(conn: &DbConn, ensemble_id: &str) -> Result> { - let mut recordings: Vec = Vec::new(); - - let rows = recordings::table - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble))) - .filter(ensembles::id.eq(ensemble_id)) - .select(recordings::table::all_columns()) - .load::(conn)?; - - for row in rows { - recordings.push(get_description_for_recording_row(conn, &row)?); - } - - Ok(recordings) -} - -/// Get allavailable information on all recordings of a work. -pub fn get_recordings_for_work(conn: &DbConn, work_id: &str) -> Result> { - let mut recordings: Vec = Vec::new(); - - let rows = recordings::table - .filter(recordings::work.eq(work_id)) - .load::(conn)?; - - for row in rows { - recordings.push(get_description_for_recording_row(conn, &row)?); - } - - Ok(recordings) -} - -/// Delete an existing recording. This will fail if there are still references to this -/// recording from other tables that are not directly part of the recording data. Also, the -/// provided user has to be allowed to delete the recording. -pub fn delete_recording(conn: &DbConn, id: &str, user: &User) -> Result<()> { - if user.may_delete() { - diesel::delete(recordings::table.filter(recordings::id.eq(id))).execute(conn)?; - Ok(()) - } else { - Err(Error::new(ServerError::Forbidden)) - } -} - -/// Get an existing recording row. -fn get_recording_row(conn: &DbConn, id: &str) -> Result> { - Ok(recordings::table - .filter(recordings::id.eq(id)) - .load::(conn)? - .into_iter() - .next()) -} - -/// Retrieve all available information on a recording from related tables. -fn get_description_for_recording_row(conn: &DbConn, row: &RecordingRow) -> Result { - let mut performances: Vec = Vec::new(); - - let performance_rows = performances::table - .filter(performances::recording.eq(&row.id)) - .load::(conn)?; - - for row in performance_rows { - performances.push(Performance { - person: match row.person { - Some(id) => { - Some(get_person(conn, &id)?.ok_or(anyhow!("No person with ID: {}", id))?) - } - None => None, - }, - ensemble: match row.ensemble { - Some(id) => { - Some(get_ensemble(conn, &id)?.ok_or(anyhow!("No ensemble with ID: {}", id))?) - } - None => None, - }, - role: match row.role { - Some(id) => Some( - get_instrument(conn, &id)?.ok_or(anyhow!("No instrument with ID: {}", id))?, - ), - None => None, - }, - }); - } - - let work = get_work(conn, &row.work)?.ok_or(anyhow!("No work with ID: {}", &row.work))?; - - let recording = Recording { - id: row.id.clone(), - work, - comment: row.comment.clone(), - performances, - }; - - Ok(recording) -} diff --git a/musicus_server/src/database/schema.rs b/musicus_server/src/database/schema.rs deleted file mode 100644 index 87846e4..0000000 --- a/musicus_server/src/database/schema.rs +++ /dev/null @@ -1,120 +0,0 @@ -table! { - ensembles (id) { - id -> Text, - name -> Text, - created_by -> Text, - } -} - -table! { - instrumentations (id) { - id -> Int8, - work -> Text, - instrument -> Text, - } -} - -table! { - instruments (id) { - id -> Text, - name -> Text, - created_by -> Text, - } -} - -table! { - performances (id) { - id -> Int8, - recording -> Text, - person -> Nullable, - ensemble -> Nullable, - role -> Nullable, - } -} - -table! { - persons (id) { - id -> Text, - first_name -> Text, - last_name -> Text, - created_by -> Text, - } -} - -table! { - recordings (id) { - id -> Text, - work -> Text, - comment -> Text, - created_by -> Text, - } -} - -table! { - users (username) { - username -> Text, - password_hash -> Text, - email -> Nullable, - is_admin -> Bool, - is_editor -> Bool, - is_banned -> Bool, - } -} - -table! { - work_parts (id) { - id -> Int8, - work -> Text, - part_index -> Int8, - title -> Text, - composer -> Nullable, - } -} - -table! { - work_sections (id) { - id -> Int8, - work -> Text, - title -> Text, - before_index -> Int8, - } -} - -table! { - works (id) { - id -> Text, - composer -> Text, - title -> Text, - created_by -> Text, - } -} - -joinable!(ensembles -> users (created_by)); -joinable!(instrumentations -> instruments (instrument)); -joinable!(instrumentations -> works (work)); -joinable!(instruments -> users (created_by)); -joinable!(performances -> ensembles (ensemble)); -joinable!(performances -> instruments (role)); -joinable!(performances -> persons (person)); -joinable!(performances -> recordings (recording)); -joinable!(persons -> users (created_by)); -joinable!(recordings -> users (created_by)); -joinable!(recordings -> works (work)); -joinable!(work_parts -> persons (composer)); -joinable!(work_parts -> works (work)); -joinable!(work_sections -> works (work)); -joinable!(works -> persons (composer)); -joinable!(works -> users (created_by)); - -allow_tables_to_appear_in_same_query!( - ensembles, - instrumentations, - instruments, - performances, - persons, - recordings, - users, - work_parts, - work_sections, - works, -); diff --git a/musicus_server/src/database/users.rs b/musicus_server/src/database/users.rs deleted file mode 100644 index 14c874b..0000000 --- a/musicus_server/src/database/users.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::schema::users; -use super::DbConn; -use anyhow::Result; -use diesel::prelude::*; -use serde::Deserialize; - -/// A user that can be authenticated to use the API. -#[derive(Insertable, Queryable, Debug, Clone)] -pub struct User { - pub username: String, - pub password_hash: String, - pub email: Option, - pub is_admin: bool, - pub is_editor: bool, - pub is_banned: bool, -} - -impl User { - /// Check whether the user is allowed to create a new item. - pub fn may_create(&self) -> bool { - !self.is_banned - } - - /// Check whether the user is allowed to edit an item created by him or somebody else. - pub fn may_edit(&self, creator: &str) -> bool { - !self.is_banned && (self.username == creator || self.is_editor) - } - - /// Check whether the user is allowed to delete an item. - pub fn may_delete(&self) -> bool { - !self.is_banned && self.is_editor - } -} - -/// A structure representing data on a user. -#[derive(AsChangeset, Deserialize, Debug, Clone)] -#[table_name = "users"] -#[serde(rename_all = "camelCase")] -pub struct UserInsertion { - pub password_hash: String, - pub email: Option, -} - -/// Insert a new user. -pub fn insert_user(conn: &DbConn, username: &str, data: &UserInsertion) -> Result<()> { - let user = User { - username: username.to_string(), - password_hash: data.password_hash.clone(), - email: data.email.clone(), - is_admin: false, - is_editor: false, - is_banned: false, - }; - diesel::insert_into(users::table) - .values(user) - .execute(conn)?; - - Ok(()) -} - -/// Update an existing user. -pub fn update_user(conn: &DbConn, username: &str, data: &UserInsertion) -> Result<()> { - diesel::update(users::table) - .filter(users::username.eq(username)) - .set(data) - .execute(conn)?; - - Ok(()) -} - -/// Get an existing user. -pub fn get_user(conn: &DbConn, username: &str) -> Result> { - Ok(users::table - .filter(users::username.eq(username)) - .load::(conn)? - .first() - .cloned()) -} diff --git a/musicus_server/src/database/works.rs b/musicus_server/src/database/works.rs deleted file mode 100644 index 20975b8..0000000 --- a/musicus_server/src/database/works.rs +++ /dev/null @@ -1,278 +0,0 @@ -use super::schema::{instrumentations, work_parts, work_sections, works}; -use super::{get_instrument, get_person, update_instrument, update_person}; -use super::{DbConn, Instrument, Person, User}; -use crate::error::ServerError; -use anyhow::{anyhow, Error, Result}; -use diesel::prelude::*; -use serde::{Deserialize, Serialize}; -use std::convert::TryInto; - -/// A specific work by a composer. -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Work { - pub id: String, - pub title: String, - pub composer: Person, - pub instruments: Vec, - pub parts: Vec, - pub sections: Vec, -} - -/// A playable part of a work. -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WorkPart { - pub title: String, - pub composer: Option, -} - -/// A heading within the work structure. -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WorkSection { - pub title: String, - pub before_index: i64, -} - -/// Table data for a work. -#[derive(Insertable, Queryable, Debug, Clone)] -#[table_name = "works"] -struct WorkRow { - pub id: String, - pub composer: String, - pub title: String, - pub created_by: String, -} - -/// Table data for an instrumentation. -#[derive(Insertable, Queryable, Debug, Clone)] -#[table_name = "instrumentations"] -struct InstrumentationRow { - pub id: i64, - pub work: String, - pub instrument: String, -} - -/// Table data for a work part. -#[derive(Insertable, Queryable, Debug, Clone)] -#[table_name = "work_parts"] -struct WorkPartRow { - pub id: i64, - pub work: String, - pub part_index: i64, - pub title: String, - pub composer: Option, -} - -/// Table data for a work section. -#[table_name = "work_sections"] -#[derive(Insertable, Queryable, Debug, Clone)] -struct WorkSectionRow { - pub id: i64, - pub work: String, - pub title: String, - pub before_index: i64, -} - -/// Update an existing work or insert a new one. This will only succeed, if the user is allowed to -/// do that. -pub fn update_work(conn: &DbConn, work: &Work, user: &User) -> Result<()> { - conn.transaction::<(), Error, _>(|| { - let old_row = get_work_row(conn, &work.id)?; - - let allowed = match old_row { - Some(row) => user.may_edit(&row.created_by), - None => user.may_create(), - }; - - if allowed { - let id = &work.id; - - // This will also delete rows from associated tables. - diesel::delete(works::table) - .filter(works::id.eq(id)) - .execute(conn)?; - - // Add associated items, if they don't already exist. - - if get_person(conn, &work.composer.id)?.is_none() { - update_person(conn, &work.composer, &user)?; - } - - for instrument in &work.instruments { - if get_instrument(conn, &instrument.id)?.is_none() { - update_instrument(conn, instrument, &user)?; - } - } - - for part in &work.parts { - if let Some(person) = &part.composer { - if get_person(conn, &person.id)?.is_none() { - update_person(conn, person, &user)?; - } - } - } - - // Add the actual work. - - let row = WorkRow { - id: id.clone(), - composer: work.composer.id.clone(), - title: work.title.clone(), - created_by: user.username.clone(), - }; - - diesel::insert_into(works::table) - .values(row) - .execute(conn)?; - - for instrument in &work.instruments { - diesel::insert_into(instrumentations::table) - .values(InstrumentationRow { - id: rand::random(), - work: id.clone(), - instrument: instrument.id.clone(), - }) - .execute(conn)?; - } - - for (index, part) in work.parts.iter().enumerate() { - let row = WorkPartRow { - id: rand::random(), - work: id.clone(), - part_index: index.try_into()?, - title: part.title.clone(), - composer: part.composer.as_ref().map(|person| person.id.clone()), - }; - - diesel::insert_into(work_parts::table) - .values(row) - .execute(conn)?; - } - - for section in &work.sections { - let row = WorkSectionRow { - id: rand::random(), - work: id.clone(), - title: section.title.clone(), - before_index: section.before_index, - }; - - diesel::insert_into(work_sections::table) - .values(row) - .execute(conn)?; - } - - Ok(()) - } else { - Err(Error::new(ServerError::Forbidden)) - } - })?; - - Ok(()) -} - -/// Get an existing work and all available information from related tables. -pub fn get_work(conn: &DbConn, id: &str) -> Result> { - let work = match get_work_row(conn, id)? { - Some(row) => Some(get_description_for_work_row(conn, &row)?), - None => None, - }; - - Ok(work) -} - -/// Delete an existing work. This will fail if there are still other tables that relate to -/// this work except for the things that are part of the information on the work itself. Also, -/// this will only succeed, if the provided user is allowed to delete the work. -pub fn delete_work(conn: &DbConn, id: &str, user: &User) -> Result<()> { - if user.may_delete() { - diesel::delete(works::table.filter(works::id.eq(id))).execute(conn)?; - Ok(()) - } else { - Err(Error::new(ServerError::Forbidden)) - } -} - -/// Get all existing works by a composer and related information from other tables. -pub fn get_works(conn: &DbConn, composer_id: &str) -> Result> { - let mut works: Vec = Vec::new(); - - let rows = works::table - .filter(works::composer.eq(composer_id)) - .load::(conn)?; - - for row in rows { - works.push(get_description_for_work_row(conn, &row)?); - } - - Ok(works) -} - -/// Get an already existing work without related rows from other tables. -fn get_work_row(conn: &DbConn, id: &str) -> Result> { - Ok(works::table - .filter(works::id.eq(id)) - .load::(conn)? - .into_iter() - .next()) -} - -/// Retrieve all available information on a work from related tables. -fn get_description_for_work_row(conn: &DbConn, row: &WorkRow) -> Result { - let mut instruments: Vec = Vec::new(); - - let instrumentations = instrumentations::table - .filter(instrumentations::work.eq(&row.id)) - .load::(conn)?; - - for instrumentation in instrumentations { - let id = instrumentation.instrument.clone(); - instruments - .push(get_instrument(conn, &id)?.ok_or(anyhow!("No instrument with ID: {}", id))?); - } - - let mut parts: Vec = Vec::new(); - - let part_rows = work_parts::table - .filter(work_parts::work.eq(&row.id)) - .load::(conn)?; - - for part_row in part_rows { - parts.push(WorkPart { - title: part_row.title, - composer: match part_row.composer { - Some(id) => { - Some(get_person(conn, &id)?.ok_or(anyhow!("No person with ID: {}", id))?) - } - None => None, - }, - }); - } - - let mut sections: Vec = Vec::new(); - - let section_rows = work_sections::table - .filter(work_sections::work.eq(&row.id)) - .load::(conn)?; - - for section in section_rows { - sections.push(WorkSection { - title: section.title, - before_index: section.before_index, - }); - } - - let id = &row.composer; - let composer = get_person(conn, id)?.ok_or(anyhow!("No person with ID: {}", id))?; - - Ok(Work { - id: row.id.clone(), - composer, - title: row.title.clone(), - instruments, - parts, - sections, - }) -} diff --git a/musicus_server/src/error.rs b/musicus_server/src/error.rs deleted file mode 100644 index 089aba8..0000000 --- a/musicus_server/src/error.rs +++ /dev/null @@ -1,50 +0,0 @@ -use actix_web::{dev::HttpResponseBuilder, error, http::StatusCode, HttpResponse}; -use derive_more::{Display, Error}; - -/// An error intended for the public interface. -#[derive(Display, Error, Debug)] -pub enum ServerError { - NotFound, - Unauthorized, - Forbidden, - Internal, -} - -impl error::ResponseError for ServerError { - fn error_response(&self) -> HttpResponse { - HttpResponseBuilder::new(self.status_code()).finish() - } - - fn status_code(&self) -> StatusCode { - match self { - ServerError::NotFound => StatusCode::NOT_FOUND, - ServerError::Unauthorized => StatusCode::UNAUTHORIZED, - ServerError::Forbidden => StatusCode::FORBIDDEN, - ServerError::Internal => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -impl From for ServerError { - fn from(_: r2d2::Error) -> Self { - ServerError::Internal - } -} - -impl From for ServerError { - fn from(error: anyhow::Error) -> Self { - match error.downcast() { - Ok(error) => error, - Err(_) => ServerError::Internal, - } - } -} - -impl From> for ServerError { - fn from(error: error::BlockingError) -> Self { - match error { - error::BlockingError::Error(error) => error, - error::BlockingError::Canceled => ServerError::Internal, - } - } -} diff --git a/musicus_server/src/main.rs b/musicus_server/src/main.rs deleted file mode 100644 index eb0edd8..0000000 --- a/musicus_server/src/main.rs +++ /dev/null @@ -1,57 +0,0 @@ -// Required for database/schema.rs -#[macro_use] -extern crate diesel; - -// Required for embed_migrations macro in database/mod.rs -#[macro_use] -extern crate diesel_migrations; - -use actix_web::{App, HttpServer}; - -mod database; -mod error; - -mod routes; -use routes::*; - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - dotenv::dotenv().ok(); - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - sodiumoxide::init().expect("Failed to init crypto library!"); - let db_pool = database::connect().expect("Failed to create database interface!"); - - let server = HttpServer::new(move || { - App::new() - .data(db_pool.clone()) - .wrap(actix_web::middleware::Logger::new( - "%t: %r -> %s; %b B; %D ms", - )) - .service(register_user) - .service(login_user) - .service(put_user) - .service(get_user) - .service(get_person) - .service(update_person) - .service(get_persons) - .service(delete_person) - .service(get_ensemble) - .service(update_ensemble) - .service(delete_ensemble) - .service(get_ensembles) - .service(get_instrument) - .service(update_instrument) - .service(delete_instrument) - .service(get_instruments) - .service(get_work) - .service(update_work) - .service(delete_work) - .service(get_works) - .service(get_recording) - .service(update_recording) - .service(delete_recording) - .service(get_recordings_for_work) - }); - - server.bind("127.0.0.1:8087")?.run().await -} diff --git a/musicus_server/src/routes/auth.rs b/musicus_server/src/routes/auth.rs deleted file mode 100644 index fa697ad..0000000 --- a/musicus_server/src/routes/auth.rs +++ /dev/null @@ -1,235 +0,0 @@ -use crate::database; -use crate::database::{DbConn, DbPool, User, UserInsertion}; -use crate::error::ServerError; -use actix_web::{get, post, put, web, HttpResponse}; -use actix_web_httpauth::extractors::bearer::BearerAuth; -use anyhow::{anyhow, Result}; -use serde::{Deserialize, Serialize}; -use sodiumoxide::crypto::pwhash::argon2id13; - -/// Request body data for user registration. -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct UserRegistration { - pub username: String, - pub password: String, - pub email: Option, -} - -/// Request body data for user login. -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Login { - pub username: String, - pub password: String, -} - -/// Request body data for changing user details. -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct PutUser { - pub old_password: String, - pub new_password: Option, - pub email: Option, -} - -/// Response body data for getting a user. -#[derive(Serialize, Debug, Clone)] -pub struct GetUser { - pub username: String, - pub email: Option, -} - -/// Claims for issued JWTs. -#[derive(Deserialize, Serialize, Debug, Clone)] -struct Claims { - pub iat: u64, - pub exp: u64, - pub username: String, -} - -/// Register a new user. -#[post("/users")] -pub async fn register_user( - db: web::Data, - data: web::Json, -) -> Result { - web::block(move || { - let conn = db.into_inner().get().or(Err(ServerError::Internal))?; - - database::insert_user( - &conn, - &data.username, - &UserInsertion { - password_hash: hash_password(&data.password).or(Err(ServerError::Internal))?, - email: data.email.clone(), - }, - ) - .or(Err(ServerError::Internal)) - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} - -/// Update an existing user. This doesn't use a JWT for authentication but requires the client to -/// resent the old password. -#[put("/users/{username}")] -pub async fn put_user( - db: web::Data, - username: web::Path, - data: web::Json, -) -> Result { - let conn = db.into_inner().get().or(Err(ServerError::Internal))?; - - web::block(move || { - let user = database::get_user(&conn, &username) - .or(Err(ServerError::Internal))? - .ok_or(ServerError::Unauthorized)?; - - if verify_password(&data.old_password, &user.password_hash) { - let password_hash = match &data.new_password { - Some(password) => hash_password(password).or(Err(ServerError::Unauthorized))?, - None => user.password_hash.clone(), - }; - - database::update_user( - &conn, - &username, - &UserInsertion { - email: data.email.clone(), - password_hash, - }, - ) - .or(Err(ServerError::Internal))?; - - Ok(()) - } else { - Err(ServerError::Forbidden) - } - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} - -/// Get an existing user. This requires a valid JWT authenticating that user. -#[get("/users/{username}")] -pub async fn get_user( - db: web::Data, - username: web::Path, - auth: BearerAuth, -) -> Result { - let user = web::block(move || { - let conn = db.into_inner().get().or(Err(ServerError::Internal))?; - authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized)) - }) - .await?; - - if username.into_inner() != user.username { - Err(ServerError::Forbidden)?; - } - - Ok(HttpResponse::Ok().json(GetUser { - username: user.username, - email: user.email, - })) -} - -/// Login an already existing user. This will respond with a newly issued JWT. -#[post("/login")] -pub async fn login_user( - db: web::Data, - data: web::Json, -) -> Result { - let token = web::block(move || { - let conn = db.into_inner().get().or(Err(ServerError::Internal))?; - - let user = database::get_user(&conn, &data.username) - .or(Err(ServerError::Internal))? - .ok_or(ServerError::Unauthorized)?; - - if verify_password(&data.password, &user.password_hash) { - issue_jwt(&user.username).or(Err(ServerError::Internal)) - } else { - Err(ServerError::Unauthorized) - } - }) - .await?; - - Ok(HttpResponse::Ok().body(token)) -} - -/// Authenticate a user by verifying the provided token. The environemtn variable "MUSICUS_SECRET" -/// will be used as the secret key and has to be set. -pub fn authenticate(conn: &DbConn, token: &str) -> Result { - let username = verify_jwt(token)?.username; - database::get_user(conn, &username)?.ok_or(anyhow!("User doesn't exist: {}", &username)) -} - -/// Return a hash for a password that can be stored in the database. -fn hash_password(password: &str) -> Result { - let hash = argon2id13::pwhash( - password.as_bytes(), - argon2id13::OPSLIMIT_INTERACTIVE, - argon2id13::MEMLIMIT_INTERACTIVE, - ) - .or(Err(anyhow!("Failed to hash password!")))?; - - // Strip trailing null bytes to facilitate database storage. - Ok(std::str::from_utf8(&hash.0)? - .trim_end_matches('\u{0}') - .to_string()) -} - -/// Verify whether a hash is valid for a password. -fn verify_password(password: &str, hash: &str) -> bool { - // Readd the trailing null bytes padding. - let mut bytes = [0u8; 128]; - for (index, byte) in hash.as_bytes().iter().enumerate() { - bytes[index] = *byte; - } - - argon2id13::pwhash_verify( - &argon2id13::HashedPassword::from_slice(&bytes).unwrap(), - password.as_bytes(), - ) -} - -/// Issue a JWT that allows to claim to be a user. This uses the value of the environment variable -/// "MUSICUS_SECRET" as the secret key. This needs to be set. -fn issue_jwt(username: &str) -> Result { - let now = std::time::SystemTime::now(); - let expiry = now + std::time::Duration::new(86400, 0); - - let iat = now.duration_since(std::time::UNIX_EPOCH)?.as_secs(); - let exp = expiry.duration_since(std::time::UNIX_EPOCH)?.as_secs(); - - let secret = std::env::var("MUSICUS_SECRET")?; - - let token = jsonwebtoken::encode( - &jsonwebtoken::Header::default(), - &Claims { - iat, - exp, - username: username.to_string(), - }, - &jsonwebtoken::EncodingKey::from_secret(&secret.as_bytes()), - )?; - - Ok(token) -} - -/// Verify a JWT and return the claims that are made by it. This uses the value of the environment -/// variable "MUSICUS_SECRET" as the secret key. This needs to be set. -fn verify_jwt(token: &str) -> Result { - let secret = std::env::var("MUSICUS_SECRET")?; - - let jwt = jsonwebtoken::decode::( - token, - &jsonwebtoken::DecodingKey::from_secret(&secret.as_bytes()), - &jsonwebtoken::Validation::default(), - )?; - - Ok(jwt.claims) -} diff --git a/musicus_server/src/routes/ensembles.rs b/musicus_server/src/routes/ensembles.rs deleted file mode 100644 index 11d671a..0000000 --- a/musicus_server/src/routes/ensembles.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::authenticate; -use crate::database; -use crate::database::{DbPool, Ensemble}; -use crate::error::ServerError; -use actix_web::{delete, get, post, web, HttpResponse}; -use actix_web_httpauth::extractors::bearer::BearerAuth; - -/// Get an existing ensemble. -#[get("/ensembles/{id}")] -pub async fn get_ensemble( - db: web::Data, - id: web::Path, -) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - database::get_ensemble(&conn, &id.into_inner())?.ok_or(ServerError::NotFound) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -/// Add a new ensemble or update an existin one. The user must be authorized to do that. -#[post("/ensembles")] -pub async fn update_ensemble( - auth: BearerAuth, - db: web::Data, - data: web::Json, -) -> Result { - web::block(move || { - let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - - database::update_ensemble(&conn, &data.into_inner(), &user)?; - - Ok(()) - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} - -#[get("/ensembles")] -pub async fn get_ensembles(db: web::Data) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - Ok(database::get_ensembles(&conn)?) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -#[delete("/ensembles/{id}")] -pub async fn delete_ensemble( - auth: BearerAuth, - db: web::Data, - id: web::Path, -) -> Result { - web::block(move || { - let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - - database::delete_ensemble(&conn, &id.into_inner(), &user)?; - - Ok(()) - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} diff --git a/musicus_server/src/routes/instruments.rs b/musicus_server/src/routes/instruments.rs deleted file mode 100644 index 2320fb6..0000000 --- a/musicus_server/src/routes/instruments.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::authenticate; -use crate::database; -use crate::database::{DbPool, Instrument}; -use crate::error::ServerError; -use actix_web::{delete, get, post, web, HttpResponse}; -use actix_web_httpauth::extractors::bearer::BearerAuth; - -/// Get an existing instrument. -#[get("/instruments/{id}")] -pub async fn get_instrument( - db: web::Data, - id: web::Path, -) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - database::get_instrument(&conn, &id.into_inner())?.ok_or(ServerError::NotFound) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -/// Add a new instrument or update an existin one. The user must be authorized to do that. -#[post("/instruments")] -pub async fn update_instrument( - auth: BearerAuth, - db: web::Data, - data: web::Json, -) -> Result { - web::block(move || { - let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - - database::update_instrument(&conn, &data.into_inner(), &user)?; - - Ok(()) - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} - -#[get("/instruments")] -pub async fn get_instruments(db: web::Data) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - Ok(database::get_instruments(&conn)?) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -#[delete("/instruments/{id}")] -pub async fn delete_instrument( - auth: BearerAuth, - db: web::Data, - id: web::Path, -) -> Result { - web::block(move || { - let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - - database::delete_instrument(&conn, &id.into_inner(), &user)?; - - Ok(()) - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} diff --git a/musicus_server/src/routes/mod.rs b/musicus_server/src/routes/mod.rs deleted file mode 100644 index c35ce5a..0000000 --- a/musicus_server/src/routes/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub mod auth; -pub use auth::*; - -pub mod ensembles; -pub use ensembles::*; - -pub mod instruments; -pub use instruments::*; - -pub mod persons; -pub use persons::*; - -pub mod recordings; -pub use recordings::*; - -pub mod works; -pub use works::*; \ No newline at end of file diff --git a/musicus_server/src/routes/persons.rs b/musicus_server/src/routes/persons.rs deleted file mode 100644 index 977e3b3..0000000 --- a/musicus_server/src/routes/persons.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::authenticate; -use crate::database; -use crate::database::{DbPool, Person}; -use crate::error::ServerError; -use actix_web::{delete, get, post, web, HttpResponse}; -use actix_web_httpauth::extractors::bearer::BearerAuth; - -/// Get an existing person. -#[get("/persons/{id}")] -pub async fn get_person( - db: web::Data, - id: web::Path, -) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - database::get_person(&conn, &id.into_inner())?.ok_or(ServerError::NotFound) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -/// Add a new person or update an existin one. The user must be authorized to do that. -#[post("/persons")] -pub async fn update_person( - auth: BearerAuth, - db: web::Data, - data: web::Json, -) -> Result { - web::block(move || { - let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - - database::update_person(&conn, &data.into_inner(), &user)?; - - Ok(()) - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} - -#[get("/persons")] -pub async fn get_persons(db: web::Data) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - Ok(database::get_persons(&conn)?) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -#[delete("/persons/{id}")] -pub async fn delete_person( - auth: BearerAuth, - db: web::Data, - id: web::Path, -) -> Result { - web::block(move || { - let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - - database::delete_person(&conn, &id.into_inner(), &user)?; - - Ok(()) - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} diff --git a/musicus_server/src/routes/recordings.rs b/musicus_server/src/routes/recordings.rs deleted file mode 100644 index c3ad273..0000000 --- a/musicus_server/src/routes/recordings.rs +++ /dev/null @@ -1,102 +0,0 @@ -use super::authenticate; -use crate::database; -use crate::database::{DbPool, Recording}; -use crate::error::ServerError; -use actix_web::{delete, get, post, web, HttpResponse}; -use actix_web_httpauth::extractors::bearer::BearerAuth; - -/// Get an existing recording. -#[get("/recordings/{id}")] -pub async fn get_recording( - db: web::Data, - id: web::Path, -) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - database::get_recording(&conn, &id.into_inner())?.ok_or(ServerError::NotFound) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -/// Add a new recording or update an existin one. The user must be authorized to do that. -#[post("/recordings")] -pub async fn update_recording( - auth: BearerAuth, - db: web::Data, - data: web::Json, -) -> Result { - web::block(move || { - let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - - database::update_recording(&conn, &data.into_inner(), &user)?; - - Ok(()) - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} - -#[get("/works/{id}/recordings")] -pub async fn get_recordings_for_work( - db: web::Data, - work_id: web::Path, -) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - Ok(database::get_recordings_for_work(&conn, &work_id.into_inner())?) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -#[get("/persons/{id}/recordings")] -pub async fn get_recordings_for_person( - db: web::Data, - person_id: web::Path, -) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - Ok(database::get_recordings_for_person(&conn, &person_id.into_inner())?) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -#[get("/ensembles/{id}/recordings")] -pub async fn get_recordings_for_ensemble( - db: web::Data, - ensemble_id: web::Path, -) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - Ok(database::get_recordings_for_ensemble(&conn, &ensemble_id.into_inner())?) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -#[delete("/recordings/{id}")] -pub async fn delete_recording( - auth: BearerAuth, - db: web::Data, - id: web::Path, -) -> Result { - web::block(move || { - let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - - database::delete_recording(&conn, &id.into_inner(), &user)?; - - Ok(()) - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} diff --git a/musicus_server/src/routes/works.rs b/musicus_server/src/routes/works.rs deleted file mode 100644 index ad8a8e9..0000000 --- a/musicus_server/src/routes/works.rs +++ /dev/null @@ -1,74 +0,0 @@ -use super::authenticate; -use crate::database; -use crate::database::{DbPool, Work}; -use crate::error::ServerError; -use actix_web::{delete, get, post, web, HttpResponse}; -use actix_web_httpauth::extractors::bearer::BearerAuth; - -/// Get an existing work. -#[get("/works/{id}")] -pub async fn get_work( - db: web::Data, - id: web::Path, -) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - database::get_work(&conn, &id.into_inner())?.ok_or(ServerError::NotFound) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -/// Add a new work or update an existin one. The user must be authorized to do that. -#[post("/works")] -pub async fn update_work( - auth: BearerAuth, - db: web::Data, - data: web::Json, -) -> Result { - web::block(move || { - let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - - database::update_work(&conn, &data.into_inner(), &user)?; - - Ok(()) - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} - -#[get("/persons/{id}/works")] -pub async fn get_works( - db: web::Data, - composer_id: web::Path, -) -> Result { - let data = web::block(move || { - let conn = db.into_inner().get()?; - Ok(database::get_works(&conn, &composer_id.into_inner())?) - }) - .await?; - - Ok(HttpResponse::Ok().json(data)) -} - -#[delete("/works/{id}")] -pub async fn delete_work( - auth: BearerAuth, - db: web::Data, - id: web::Path, -) -> Result { - web::block(move || { - let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - - database::delete_work(&conn, &id.into_inner(), &user)?; - - Ok(()) - }) - .await?; - - Ok(HttpResponse::Ok().finish()) -} diff --git a/musicus/po/LINGUAS b/po/LINGUAS similarity index 100% rename from musicus/po/LINGUAS rename to po/LINGUAS diff --git a/musicus/po/POTFILES.in b/po/POTFILES.in similarity index 100% rename from musicus/po/POTFILES.in rename to po/POTFILES.in diff --git a/musicus/po/de.po b/po/de.po similarity index 100% rename from musicus/po/de.po rename to po/de.po diff --git a/musicus/po/meson.build b/po/meson.build similarity index 100% rename from musicus/po/meson.build rename to po/meson.build diff --git a/musicus/po/musicus.pot b/po/musicus.pot similarity index 100% rename from musicus/po/musicus.pot rename to po/musicus.pot diff --git a/musicus/res/meson.build b/res/meson.build similarity index 100% rename from musicus/res/meson.build rename to res/meson.build diff --git a/musicus/res/musicus.gresource.xml b/res/musicus.gresource.xml similarity index 100% rename from musicus/res/musicus.gresource.xml rename to res/musicus.gresource.xml diff --git a/musicus/res/ui/ensemble_editor.ui b/res/ui/ensemble_editor.ui similarity index 100% rename from musicus/res/ui/ensemble_editor.ui rename to res/ui/ensemble_editor.ui diff --git a/musicus/res/ui/ensemble_screen.ui b/res/ui/ensemble_screen.ui similarity index 100% rename from musicus/res/ui/ensemble_screen.ui rename to res/ui/ensemble_screen.ui diff --git a/musicus/res/ui/ensemble_selector.ui b/res/ui/ensemble_selector.ui similarity index 100% rename from musicus/res/ui/ensemble_selector.ui rename to res/ui/ensemble_selector.ui diff --git a/musicus/res/ui/instrument_editor.ui b/res/ui/instrument_editor.ui similarity index 100% rename from musicus/res/ui/instrument_editor.ui rename to res/ui/instrument_editor.ui diff --git a/musicus/res/ui/instrument_selector.ui b/res/ui/instrument_selector.ui similarity index 100% rename from musicus/res/ui/instrument_selector.ui rename to res/ui/instrument_selector.ui diff --git a/musicus/res/ui/login_dialog.ui b/res/ui/login_dialog.ui similarity index 100% rename from musicus/res/ui/login_dialog.ui rename to res/ui/login_dialog.ui diff --git a/musicus/res/ui/medium_editor.ui b/res/ui/medium_editor.ui similarity index 100% rename from musicus/res/ui/medium_editor.ui rename to res/ui/medium_editor.ui diff --git a/musicus/res/ui/performance_editor.ui b/res/ui/performance_editor.ui similarity index 100% rename from musicus/res/ui/performance_editor.ui rename to res/ui/performance_editor.ui diff --git a/musicus/res/ui/person_editor.ui b/res/ui/person_editor.ui similarity index 100% rename from musicus/res/ui/person_editor.ui rename to res/ui/person_editor.ui diff --git a/musicus/res/ui/person_list.ui b/res/ui/person_list.ui similarity index 100% rename from musicus/res/ui/person_list.ui rename to res/ui/person_list.ui diff --git a/musicus/res/ui/person_screen.ui b/res/ui/person_screen.ui similarity index 100% rename from musicus/res/ui/person_screen.ui rename to res/ui/person_screen.ui diff --git a/musicus/res/ui/person_selector.ui b/res/ui/person_selector.ui similarity index 100% rename from musicus/res/ui/person_selector.ui rename to res/ui/person_selector.ui diff --git a/musicus/res/ui/player_bar.ui b/res/ui/player_bar.ui similarity index 100% rename from musicus/res/ui/player_bar.ui rename to res/ui/player_bar.ui diff --git a/musicus/res/ui/player_screen.ui b/res/ui/player_screen.ui similarity index 100% rename from musicus/res/ui/player_screen.ui rename to res/ui/player_screen.ui diff --git a/musicus/res/ui/poe_list.ui b/res/ui/poe_list.ui similarity index 100% rename from musicus/res/ui/poe_list.ui rename to res/ui/poe_list.ui diff --git a/musicus/res/ui/preferences.ui b/res/ui/preferences.ui similarity index 100% rename from musicus/res/ui/preferences.ui rename to res/ui/preferences.ui diff --git a/musicus/res/ui/recording_editor.ui b/res/ui/recording_editor.ui similarity index 100% rename from musicus/res/ui/recording_editor.ui rename to res/ui/recording_editor.ui diff --git a/musicus/res/ui/recording_screen.ui b/res/ui/recording_screen.ui similarity index 100% rename from musicus/res/ui/recording_screen.ui rename to res/ui/recording_screen.ui diff --git a/musicus/res/ui/recording_selector.ui b/res/ui/recording_selector.ui similarity index 100% rename from musicus/res/ui/recording_selector.ui rename to res/ui/recording_selector.ui diff --git a/musicus/res/ui/recording_selector_screen.ui b/res/ui/recording_selector_screen.ui similarity index 100% rename from musicus/res/ui/recording_selector_screen.ui rename to res/ui/recording_selector_screen.ui diff --git a/musicus/res/ui/selector.ui b/res/ui/selector.ui similarity index 100% rename from musicus/res/ui/selector.ui rename to res/ui/selector.ui diff --git a/musicus/res/ui/server_dialog.ui b/res/ui/server_dialog.ui similarity index 100% rename from musicus/res/ui/server_dialog.ui rename to res/ui/server_dialog.ui diff --git a/musicus/res/ui/source_selector.ui b/res/ui/source_selector.ui similarity index 100% rename from musicus/res/ui/source_selector.ui rename to res/ui/source_selector.ui diff --git a/musicus/res/ui/track_editor.ui b/res/ui/track_editor.ui similarity index 100% rename from musicus/res/ui/track_editor.ui rename to res/ui/track_editor.ui diff --git a/musicus/res/ui/track_selector.ui b/res/ui/track_selector.ui similarity index 100% rename from musicus/res/ui/track_selector.ui rename to res/ui/track_selector.ui diff --git a/musicus/res/ui/track_set_editor.ui b/res/ui/track_set_editor.ui similarity index 100% rename from musicus/res/ui/track_set_editor.ui rename to res/ui/track_set_editor.ui diff --git a/musicus/res/ui/window.ui b/res/ui/window.ui similarity index 100% rename from musicus/res/ui/window.ui rename to res/ui/window.ui diff --git a/musicus/res/ui/work_editor.ui b/res/ui/work_editor.ui similarity index 100% rename from musicus/res/ui/work_editor.ui rename to res/ui/work_editor.ui diff --git a/musicus/res/ui/work_part_editor.ui b/res/ui/work_part_editor.ui similarity index 100% rename from musicus/res/ui/work_part_editor.ui rename to res/ui/work_part_editor.ui diff --git a/musicus/res/ui/work_screen.ui b/res/ui/work_screen.ui similarity index 100% rename from musicus/res/ui/work_screen.ui rename to res/ui/work_screen.ui diff --git a/musicus/res/ui/work_section_editor.ui b/res/ui/work_section_editor.ui similarity index 100% rename from musicus/res/ui/work_section_editor.ui rename to res/ui/work_section_editor.ui diff --git a/musicus/res/ui/work_selector.ui b/res/ui/work_selector.ui similarity index 100% rename from musicus/res/ui/work_selector.ui rename to res/ui/work_selector.ui diff --git a/musicus/res/ui/work_selector_screen.ui b/res/ui/work_selector_screen.ui similarity index 100% rename from musicus/res/ui/work_selector_screen.ui rename to res/ui/work_selector_screen.ui diff --git a/musicus/src/backend/client/ensembles.rs b/src/backend/client/ensembles.rs similarity index 100% rename from musicus/src/backend/client/ensembles.rs rename to src/backend/client/ensembles.rs diff --git a/musicus/src/backend/client/instruments.rs b/src/backend/client/instruments.rs similarity index 100% rename from musicus/src/backend/client/instruments.rs rename to src/backend/client/instruments.rs diff --git a/musicus/src/backend/client/mod.rs b/src/backend/client/mod.rs similarity index 100% rename from musicus/src/backend/client/mod.rs rename to src/backend/client/mod.rs diff --git a/musicus/src/backend/client/persons.rs b/src/backend/client/persons.rs similarity index 100% rename from musicus/src/backend/client/persons.rs rename to src/backend/client/persons.rs diff --git a/musicus/src/backend/client/recordings.rs b/src/backend/client/recordings.rs similarity index 100% rename from musicus/src/backend/client/recordings.rs rename to src/backend/client/recordings.rs diff --git a/musicus/src/backend/client/works.rs b/src/backend/client/works.rs similarity index 100% rename from musicus/src/backend/client/works.rs rename to src/backend/client/works.rs diff --git a/musicus/src/backend/library.rs b/src/backend/library.rs similarity index 100% rename from musicus/src/backend/library.rs rename to src/backend/library.rs diff --git a/musicus/src/backend/mod.rs b/src/backend/mod.rs similarity index 100% rename from musicus/src/backend/mod.rs rename to src/backend/mod.rs diff --git a/musicus/src/backend/secure.rs b/src/backend/secure.rs similarity index 100% rename from musicus/src/backend/secure.rs rename to src/backend/secure.rs diff --git a/musicus/src/config.rs.in b/src/config.rs.in similarity index 100% rename from musicus/src/config.rs.in rename to src/config.rs.in diff --git a/musicus/src/database/ensembles.rs b/src/database/ensembles.rs similarity index 100% rename from musicus/src/database/ensembles.rs rename to src/database/ensembles.rs diff --git a/musicus/src/database/instruments.rs b/src/database/instruments.rs similarity index 100% rename from musicus/src/database/instruments.rs rename to src/database/instruments.rs diff --git a/musicus/src/database/medium.rs b/src/database/medium.rs similarity index 100% rename from musicus/src/database/medium.rs rename to src/database/medium.rs diff --git a/musicus/src/database/mod.rs b/src/database/mod.rs similarity index 100% rename from musicus/src/database/mod.rs rename to src/database/mod.rs diff --git a/musicus/src/database/persons.rs b/src/database/persons.rs similarity index 100% rename from musicus/src/database/persons.rs rename to src/database/persons.rs diff --git a/musicus/src/database/recordings.rs b/src/database/recordings.rs similarity index 100% rename from musicus/src/database/recordings.rs rename to src/database/recordings.rs diff --git a/musicus/src/database/schema.rs b/src/database/schema.rs similarity index 100% rename from musicus/src/database/schema.rs rename to src/database/schema.rs diff --git a/musicus/src/database/thread.rs b/src/database/thread.rs similarity index 100% rename from musicus/src/database/thread.rs rename to src/database/thread.rs diff --git a/musicus/src/database/works.rs b/src/database/works.rs similarity index 100% rename from musicus/src/database/works.rs rename to src/database/works.rs diff --git a/musicus/src/dialogs/about.rs b/src/dialogs/about.rs similarity index 100% rename from musicus/src/dialogs/about.rs rename to src/dialogs/about.rs diff --git a/musicus/src/dialogs/login_dialog.rs b/src/dialogs/login_dialog.rs similarity index 100% rename from musicus/src/dialogs/login_dialog.rs rename to src/dialogs/login_dialog.rs diff --git a/musicus/src/dialogs/mod.rs b/src/dialogs/mod.rs similarity index 100% rename from musicus/src/dialogs/mod.rs rename to src/dialogs/mod.rs diff --git a/musicus/src/dialogs/preferences.rs b/src/dialogs/preferences.rs similarity index 100% rename from musicus/src/dialogs/preferences.rs rename to src/dialogs/preferences.rs diff --git a/musicus/src/dialogs/server_dialog.rs b/src/dialogs/server_dialog.rs similarity index 100% rename from musicus/src/dialogs/server_dialog.rs rename to src/dialogs/server_dialog.rs diff --git a/musicus/src/editors/ensemble.rs b/src/editors/ensemble.rs similarity index 100% rename from musicus/src/editors/ensemble.rs rename to src/editors/ensemble.rs diff --git a/musicus/src/editors/instrument.rs b/src/editors/instrument.rs similarity index 100% rename from musicus/src/editors/instrument.rs rename to src/editors/instrument.rs diff --git a/musicus/src/editors/mod.rs b/src/editors/mod.rs similarity index 100% rename from musicus/src/editors/mod.rs rename to src/editors/mod.rs diff --git a/musicus/src/editors/performance.rs b/src/editors/performance.rs similarity index 100% rename from musicus/src/editors/performance.rs rename to src/editors/performance.rs diff --git a/musicus/src/editors/person.rs b/src/editors/person.rs similarity index 100% rename from musicus/src/editors/person.rs rename to src/editors/person.rs diff --git a/musicus/src/editors/recording.rs b/src/editors/recording.rs similarity index 100% rename from musicus/src/editors/recording.rs rename to src/editors/recording.rs diff --git a/musicus/src/editors/work.rs b/src/editors/work.rs similarity index 100% rename from musicus/src/editors/work.rs rename to src/editors/work.rs diff --git a/musicus/src/editors/work_part.rs b/src/editors/work_part.rs similarity index 100% rename from musicus/src/editors/work_part.rs rename to src/editors/work_part.rs diff --git a/musicus/src/editors/work_section.rs b/src/editors/work_section.rs similarity index 100% rename from musicus/src/editors/work_section.rs rename to src/editors/work_section.rs diff --git a/musicus/src/import/disc_source.rs b/src/import/disc_source.rs similarity index 100% rename from musicus/src/import/disc_source.rs rename to src/import/disc_source.rs diff --git a/musicus/src/import/medium_editor.rs b/src/import/medium_editor.rs similarity index 100% rename from musicus/src/import/medium_editor.rs rename to src/import/medium_editor.rs diff --git a/musicus/src/import/mod.rs b/src/import/mod.rs similarity index 100% rename from musicus/src/import/mod.rs rename to src/import/mod.rs diff --git a/musicus/src/import/source_selector.rs b/src/import/source_selector.rs similarity index 100% rename from musicus/src/import/source_selector.rs rename to src/import/source_selector.rs diff --git a/musicus/src/import/track_editor.rs b/src/import/track_editor.rs similarity index 100% rename from musicus/src/import/track_editor.rs rename to src/import/track_editor.rs diff --git a/musicus/src/import/track_selector.rs b/src/import/track_selector.rs similarity index 100% rename from musicus/src/import/track_selector.rs rename to src/import/track_selector.rs diff --git a/musicus/src/import/track_set_editor.rs b/src/import/track_set_editor.rs similarity index 100% rename from musicus/src/import/track_set_editor.rs rename to src/import/track_set_editor.rs diff --git a/musicus/src/main.rs b/src/main.rs similarity index 100% rename from musicus/src/main.rs rename to src/main.rs diff --git a/musicus/src/meson.build b/src/meson.build similarity index 100% rename from musicus/src/meson.build rename to src/meson.build diff --git a/musicus/src/player.rs b/src/player.rs similarity index 100% rename from musicus/src/player.rs rename to src/player.rs diff --git a/musicus/src/resources.rs.in b/src/resources.rs.in similarity index 100% rename from musicus/src/resources.rs.in rename to src/resources.rs.in diff --git a/musicus/src/screens/ensemble_screen.rs b/src/screens/ensemble_screen.rs similarity index 100% rename from musicus/src/screens/ensemble_screen.rs rename to src/screens/ensemble_screen.rs diff --git a/musicus/src/screens/mod.rs b/src/screens/mod.rs similarity index 100% rename from musicus/src/screens/mod.rs rename to src/screens/mod.rs diff --git a/musicus/src/screens/person_screen.rs b/src/screens/person_screen.rs similarity index 100% rename from musicus/src/screens/person_screen.rs rename to src/screens/person_screen.rs diff --git a/musicus/src/screens/player_screen.rs b/src/screens/player_screen.rs similarity index 100% rename from musicus/src/screens/player_screen.rs rename to src/screens/player_screen.rs diff --git a/musicus/src/screens/recording_screen.rs b/src/screens/recording_screen.rs similarity index 100% rename from musicus/src/screens/recording_screen.rs rename to src/screens/recording_screen.rs diff --git a/musicus/src/screens/work_screen.rs b/src/screens/work_screen.rs similarity index 100% rename from musicus/src/screens/work_screen.rs rename to src/screens/work_screen.rs diff --git a/musicus/src/selectors/ensemble.rs b/src/selectors/ensemble.rs similarity index 100% rename from musicus/src/selectors/ensemble.rs rename to src/selectors/ensemble.rs diff --git a/musicus/src/selectors/instrument.rs b/src/selectors/instrument.rs similarity index 100% rename from musicus/src/selectors/instrument.rs rename to src/selectors/instrument.rs diff --git a/musicus/src/selectors/mod.rs b/src/selectors/mod.rs similarity index 100% rename from musicus/src/selectors/mod.rs rename to src/selectors/mod.rs diff --git a/musicus/src/selectors/person.rs b/src/selectors/person.rs similarity index 100% rename from musicus/src/selectors/person.rs rename to src/selectors/person.rs diff --git a/musicus/src/selectors/recording.rs b/src/selectors/recording.rs similarity index 100% rename from musicus/src/selectors/recording.rs rename to src/selectors/recording.rs diff --git a/musicus/src/selectors/selector.rs b/src/selectors/selector.rs similarity index 100% rename from musicus/src/selectors/selector.rs rename to src/selectors/selector.rs diff --git a/musicus/src/selectors/work.rs b/src/selectors/work.rs similarity index 100% rename from musicus/src/selectors/work.rs rename to src/selectors/work.rs diff --git a/musicus/src/widgets/list.rs b/src/widgets/list.rs similarity index 100% rename from musicus/src/widgets/list.rs rename to src/widgets/list.rs diff --git a/musicus/src/widgets/mod.rs b/src/widgets/mod.rs similarity index 100% rename from musicus/src/widgets/mod.rs rename to src/widgets/mod.rs diff --git a/musicus/src/widgets/navigator.rs b/src/widgets/navigator.rs similarity index 100% rename from musicus/src/widgets/navigator.rs rename to src/widgets/navigator.rs diff --git a/musicus/src/widgets/navigator_window.rs b/src/widgets/navigator_window.rs similarity index 100% rename from musicus/src/widgets/navigator_window.rs rename to src/widgets/navigator_window.rs diff --git a/musicus/src/widgets/new_list.rs b/src/widgets/new_list.rs similarity index 100% rename from musicus/src/widgets/new_list.rs rename to src/widgets/new_list.rs diff --git a/musicus/src/widgets/player_bar.rs b/src/widgets/player_bar.rs similarity index 100% rename from musicus/src/widgets/player_bar.rs rename to src/widgets/player_bar.rs diff --git a/musicus/src/widgets/poe_list.rs b/src/widgets/poe_list.rs similarity index 100% rename from musicus/src/widgets/poe_list.rs rename to src/widgets/poe_list.rs diff --git a/musicus/src/widgets/selector_row.rs b/src/widgets/selector_row.rs similarity index 100% rename from musicus/src/widgets/selector_row.rs rename to src/widgets/selector_row.rs diff --git a/musicus/src/window.rs b/src/window.rs similarity index 100% rename from musicus/src/window.rs rename to src/window.rs