mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 11:47:25 +01:00
Revert merging of server and client repository
This commit is contained in:
parent
2b9cff885b
commit
8c3c439409
147 changed files with 53 additions and 2113 deletions
0
musicus/.gitignore → .gitignore
vendored
0
musicus/.gitignore → .gitignore
vendored
59
README.md
59
README.md
|
|
@ -1,16 +1,63 @@
|
||||||
# Musicus
|
# Musicus
|
||||||
|
|
||||||
The classical music player and organizer.
|
This is a desktop app for Musicus.
|
||||||
|
|
||||||
https://musicus.org
|
https://musicus.org
|
||||||
|
|
||||||
## Repository structure
|
## Hacking
|
||||||
|
|
||||||
The subdirectories contain toplevel components of the Musicus system. Currently
|
### Building
|
||||||
you will find:
|
|
||||||
|
|
||||||
* `musicus` – Musicus desktop app for Linux
|
Musicus uses the [Meson build system](https://mesonbuild.com/). You can build
|
||||||
* `musicus_server` – Musicus server
|
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
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
|
@ -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/.
|
|
||||||
3
musicus_server/.gitignore
vendored
3
musicus_server/.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
||||||
/.env
|
|
||||||
/Cargo.lock
|
|
||||||
/target
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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/.
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[print_schema]
|
|
||||||
file = "src/database/schema.rs"
|
|
||||||
|
|
@ -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();
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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)
|
|
||||||
);
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
);
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
|
|
@ -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::*;
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue