Revert merging of server and client repository

This commit is contained in:
Elias Projahn 2021-01-16 16:15:08 +01:00
parent 2b9cff885b
commit 8c3c439409
147 changed files with 53 additions and 2113 deletions

View file

View file

@ -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

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

@ -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/.

View file

@ -1,3 +0,0 @@
/.env
/Cargo.lock
/target

View file

@ -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"

View file

@ -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/.

View file

@ -1,2 +0,0 @@
[print_schema]
file = "src/database/schema.rs"

View file

@ -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();

View file

@ -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;

View file

@ -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;

View file

@ -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)
);

View file

@ -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<EnsembleRow> 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<Option<Ensemble>> {
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<Vec<Ensemble>> {
let rows = ensembles::table.load::<EnsembleRow>(conn)?;
let ensembles: Vec<Ensemble> = 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<Option<EnsembleRow>> {
let row = ensembles::table
.filter(ensembles::id.eq(id))
.load::<EnsembleRow>(conn)?
.into_iter()
.next();
Ok(row)
}

View file

@ -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<InstrumentRow> 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<Option<Instrument>> {
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<Vec<Instrument>> {
let rows = instruments::table.load::<InstrumentRow>(conn)?;
let instruments: Vec<Instrument> = 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<Option<InstrumentRow>> {
let row = instruments::table
.filter(instruments::id.eq(id))
.load::<InstrumentRow>(conn)?
.into_iter()
.next();
Ok(row)
}

View file

@ -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<r2d2::ConnectionManager<PgConnection>>;
/// One database connection from the connection pool.
pub type DbConn = r2d2::PooledConnection<r2d2::ConnectionManager<PgConnection>>;
/// 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<DbPool> {
let url = std::env::var("MUSICUS_DATABASE_URL")?;
let manager = r2d2::ConnectionManager::<PgConnection>::new(url);
let pool = r2d2::Pool::new(manager)?;
// Run embedded migrations.
let conn = pool.get()?;
embedded_migrations::run(&conn)?;
Ok(pool)
}

View file

@ -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<PersonRow> 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<Option<Person>> {
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<Vec<Person>> {
let rows = persons::table.load::<PersonRow>(conn)?;
let persons: Vec<Person> = 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<Option<PersonRow>> {
let row = persons::table
.filter(persons::id.eq(id))
.load::<PersonRow>(conn)?
.into_iter()
.next();
Ok(row)
}

View file

@ -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<Performance>,
}
/// 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<Person>,
pub ensemble: Option<Ensemble>,
pub role: Option<Instrument>,
}
/// 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<String>,
pub ensemble: Option<String>,
pub role: Option<String>,
}
/// 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<Option<Recording>> {
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<Vec<Recording>> {
let mut recordings: Vec<Recording> = 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::<RecordingRow>(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<Vec<Recording>> {
let mut recordings: Vec<Recording> = 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::<RecordingRow>(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<Vec<Recording>> {
let mut recordings: Vec<Recording> = Vec::new();
let rows = recordings::table
.filter(recordings::work.eq(work_id))
.load::<RecordingRow>(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<Option<RecordingRow>> {
Ok(recordings::table
.filter(recordings::id.eq(id))
.load::<RecordingRow>(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<Recording> {
let mut performances: Vec<Performance> = Vec::new();
let performance_rows = performances::table
.filter(performances::recording.eq(&row.id))
.load::<PerformanceRow>(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)
}

View file

@ -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<Text>,
ensemble -> Nullable<Text>,
role -> Nullable<Text>,
}
}
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<Text>,
is_admin -> Bool,
is_editor -> Bool,
is_banned -> Bool,
}
}
table! {
work_parts (id) {
id -> Int8,
work -> Text,
part_index -> Int8,
title -> Text,
composer -> Nullable<Text>,
}
}
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,
);

View file

@ -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<String>,
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<String>,
}
/// 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<Option<User>> {
Ok(users::table
.filter(users::username.eq(username))
.load::<User>(conn)?
.first()
.cloned())
}

View file

@ -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<Instrument>,
pub parts: Vec<WorkPart>,
pub sections: Vec<WorkSection>,
}
/// A playable part of a work.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct WorkPart {
pub title: String,
pub composer: Option<Person>,
}
/// 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<String>,
}
/// 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<Option<Work>> {
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<Vec<Work>> {
let mut works: Vec<Work> = Vec::new();
let rows = works::table
.filter(works::composer.eq(composer_id))
.load::<WorkRow>(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<Option<WorkRow>> {
Ok(works::table
.filter(works::id.eq(id))
.load::<WorkRow>(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<Work> {
let mut instruments: Vec<Instrument> = Vec::new();
let instrumentations = instrumentations::table
.filter(instrumentations::work.eq(&row.id))
.load::<InstrumentationRow>(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<WorkPart> = Vec::new();
let part_rows = work_parts::table
.filter(work_parts::work.eq(&row.id))
.load::<WorkPartRow>(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<WorkSection> = Vec::new();
let section_rows = work_sections::table
.filter(work_sections::work.eq(&row.id))
.load::<WorkSectionRow>(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,
})
}

View file

@ -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<r2d2::Error> for ServerError {
fn from(_: r2d2::Error) -> Self {
ServerError::Internal
}
}
impl From<anyhow::Error> for ServerError {
fn from(error: anyhow::Error) -> Self {
match error.downcast() {
Ok(error) => error,
Err(_) => ServerError::Internal,
}
}
}
impl From<error::BlockingError<ServerError>> for ServerError {
fn from(error: error::BlockingError<ServerError>) -> Self {
match error {
error::BlockingError::Error(error) => error,
error::BlockingError::Canceled => ServerError::Internal,
}
}
}

View file

@ -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
}

View file

@ -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<String>,
}
/// 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<String>,
pub email: Option<String>,
}
/// Response body data for getting a user.
#[derive(Serialize, Debug, Clone)]
pub struct GetUser {
pub username: String,
pub email: Option<String>,
}
/// 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<DbPool>,
data: web::Json<UserRegistration>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
username: web::Path<String>,
data: web::Json<PutUser>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
username: web::Path<String>,
auth: BearerAuth,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
data: web::Json<Login>,
) -> Result<HttpResponse, ServerError> {
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<User> {
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<String> {
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<String> {
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<Claims> {
let secret = std::env::var("MUSICUS_SECRET")?;
let jwt = jsonwebtoken::decode::<Claims>(
token,
&jsonwebtoken::DecodingKey::from_secret(&secret.as_bytes()),
&jsonwebtoken::Validation::default(),
)?;
Ok(jwt.claims)
}

View file

@ -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<DbPool>,
id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
data: web::Json<Ensemble>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>) -> Result<HttpResponse, ServerError> {
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<DbPool>,
id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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())
}

View file

@ -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<DbPool>,
id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
data: web::Json<Instrument>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>) -> Result<HttpResponse, ServerError> {
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<DbPool>,
id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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())
}

View file

@ -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::*;

View file

@ -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<DbPool>,
id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
data: web::Json<Person>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>) -> Result<HttpResponse, ServerError> {
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<DbPool>,
id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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())
}

View file

@ -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<DbPool>,
id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
data: web::Json<Recording>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
work_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
person_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
ensemble_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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())
}

View file

@ -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<DbPool>,
id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
data: web::Json<Work>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
composer_id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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<DbPool>,
id: web::Path<String>,
) -> Result<HttpResponse, ServerError> {
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())
}

Some files were not shown because too many files have changed in this diff Show more