mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 11:47:25 +01:00
Add server
This commit is contained in:
parent
775f3ffe90
commit
d0c25531d3
27 changed files with 1725 additions and 3 deletions
|
|
@ -7,8 +7,10 @@ https://musicus.org
|
||||||
## Repository structure
|
## Repository structure
|
||||||
|
|
||||||
The subdirectories contain toplevel components of the Musicus system. Currently
|
The subdirectories contain toplevel components of the Musicus system. Currently
|
||||||
you will only find a Musicus desktop app under `musicus`. The component READMEs
|
you will find:
|
||||||
provide more detailed information.
|
|
||||||
|
* `musicus` – Musicus desktop app for Linux
|
||||||
|
* `musicus_server` – Musicus server
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,3 +58,18 @@ $ diesel migration run --database-url test.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
This file should never be edited manually.
|
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
Normal file
3
musicus_server/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/.env
|
||||||
|
/Cargo.lock
|
||||||
|
/target
|
||||||
19
musicus_server/Cargo.toml
Normal file
19
musicus_server/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[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"] }
|
||||||
|
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"
|
||||||
50
musicus_server/README.md
Normal file
50
musicus_server/README.md
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
# 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/.
|
||||||
2
musicus_server/diesel.toml
Normal file
2
musicus_server/diesel.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[print_schema]
|
||||||
|
file = "src/database/schema.rs"
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- 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();
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
-- 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;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
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 BIGINT 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 BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_by TEXT NOT NULL REFERENCES users(username)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE works (
|
||||||
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
composer BIGINT 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 BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
||||||
|
instrument BIGINT NOT NULL REFERENCES instruments(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE work_parts (
|
||||||
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
||||||
|
part_index BIGINT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
composer BIGINT REFERENCES persons(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE work_sections (
|
||||||
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
work BIGINT NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
before_index BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ensembles (
|
||||||
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_by TEXT NOT NULL REFERENCES users(username)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE recordings (
|
||||||
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
work BIGINT 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 BIGINT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE,
|
||||||
|
person BIGINT REFERENCES persons(id),
|
||||||
|
ensemble BIGINT REFERENCES ensembles(id),
|
||||||
|
role BIGINT REFERENCES instruments(id)
|
||||||
|
);
|
||||||
75
musicus_server/src/database/ensembles.rs
Normal file
75
musicus_server/src/database/ensembles.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
use super::schema::ensembles;
|
||||||
|
use super::DbConn;
|
||||||
|
use anyhow::Result;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::{Insertable, Queryable};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// An ensemble that takes part in recordings.
|
||||||
|
#[derive(Insertable, Queryable, Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Ensemble {
|
||||||
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub created_by: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure representing data on an ensemble.
|
||||||
|
#[derive(AsChangeset, Deserialize, Debug, Clone)]
|
||||||
|
#[table_name = "ensembles"]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EnsembleInsertion {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a new ensemble.
|
||||||
|
pub fn insert_ensemble(
|
||||||
|
conn: &DbConn,
|
||||||
|
id: u32,
|
||||||
|
data: &EnsembleInsertion,
|
||||||
|
created_by: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let ensemble = Ensemble {
|
||||||
|
id: id as i64,
|
||||||
|
name: data.name.clone(),
|
||||||
|
created_by: created_by.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(ensembles::table)
|
||||||
|
.values(ensemble)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an existing ensemble.
|
||||||
|
pub fn update_ensemble(conn: &DbConn, id: u32, data: &EnsembleInsertion) -> Result<()> {
|
||||||
|
diesel::update(ensembles::table)
|
||||||
|
.filter(ensembles::id.eq(id as i64))
|
||||||
|
.set(data)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an existing ensemble.
|
||||||
|
pub fn get_ensemble(conn: &DbConn, id: u32) -> Result<Option<Ensemble>> {
|
||||||
|
Ok(ensembles::table
|
||||||
|
.filter(ensembles::id.eq(id as i64))
|
||||||
|
.load::<Ensemble>(conn)?
|
||||||
|
.first()
|
||||||
|
.cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete an existing ensemble.
|
||||||
|
pub fn delete_ensemble(conn: &DbConn, id: u32) -> Result<()> {
|
||||||
|
diesel::delete(ensembles::table.filter(ensembles::id.eq(id as i64))).execute(conn)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all existing ensembles.
|
||||||
|
pub fn get_ensembles(conn: &DbConn) -> Result<Vec<Ensemble>> {
|
||||||
|
Ok(ensembles::table.load::<Ensemble>(conn)?)
|
||||||
|
}
|
||||||
75
musicus_server/src/database/instruments.rs
Normal file
75
musicus_server/src/database/instruments.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
use super::schema::instruments;
|
||||||
|
use super::DbConn;
|
||||||
|
use anyhow::Result;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::{Insertable, Queryable};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// An instrument or any other possible role within a recording.
|
||||||
|
#[derive(Insertable, Queryable, Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Instrument {
|
||||||
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub created_by: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure representing data on an instrument.
|
||||||
|
#[derive(AsChangeset, Deserialize, Debug, Clone)]
|
||||||
|
#[table_name = "instruments"]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct InstrumentInsertion {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a new instrument.
|
||||||
|
pub fn insert_instrument(
|
||||||
|
conn: &DbConn,
|
||||||
|
id: u32,
|
||||||
|
data: &InstrumentInsertion,
|
||||||
|
created_by: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let instrument = Instrument {
|
||||||
|
id: id as i64,
|
||||||
|
name: data.name.clone(),
|
||||||
|
created_by: created_by.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(instruments::table)
|
||||||
|
.values(instrument)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an existing instrument.
|
||||||
|
pub fn update_instrument(conn: &DbConn, id: u32, data: &InstrumentInsertion) -> Result<()> {
|
||||||
|
diesel::update(instruments::table)
|
||||||
|
.filter(instruments::id.eq(id as i64))
|
||||||
|
.set(data)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an existing instrument.
|
||||||
|
pub fn get_instrument(conn: &DbConn, id: u32) -> Result<Option<Instrument>> {
|
||||||
|
Ok(instruments::table
|
||||||
|
.filter(instruments::id.eq(id as i64))
|
||||||
|
.load::<Instrument>(conn)?
|
||||||
|
.first()
|
||||||
|
.cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete an existing instrument.
|
||||||
|
pub fn delete_instrument(conn: &DbConn, id: u32) -> Result<()> {
|
||||||
|
diesel::delete(instruments::table.filter(instruments::id.eq(id as i64))).execute(conn)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all existing instruments.
|
||||||
|
pub fn get_instruments(conn: &DbConn) -> Result<Vec<Instrument>> {
|
||||||
|
Ok(instruments::table.load::<Instrument>(conn)?)
|
||||||
|
}
|
||||||
39
musicus_server/src/database/mod.rs
Normal file
39
musicus_server/src/database/mod.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// 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)?;
|
||||||
|
|
||||||
|
Ok(pool)
|
||||||
|
}
|
||||||
78
musicus_server/src/database/persons.rs
Normal file
78
musicus_server/src/database/persons.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
use super::schema::persons;
|
||||||
|
use super::DbConn;
|
||||||
|
use anyhow::Result;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::{Insertable, Queryable};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A person that is a composer, an interpret or both.
|
||||||
|
#[derive(Insertable, Queryable, Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Person {
|
||||||
|
pub id: i64,
|
||||||
|
pub first_name: String,
|
||||||
|
pub last_name: String,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub created_by: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure representing data on a person.
|
||||||
|
#[derive(AsChangeset, Deserialize, Debug, Clone)]
|
||||||
|
#[table_name = "persons"]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PersonInsertion {
|
||||||
|
pub first_name: String,
|
||||||
|
pub last_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a new person.
|
||||||
|
pub fn insert_person(
|
||||||
|
conn: &DbConn,
|
||||||
|
id: u32,
|
||||||
|
data: &PersonInsertion,
|
||||||
|
created_by: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let person = Person {
|
||||||
|
id: id as i64,
|
||||||
|
first_name: data.first_name.clone(),
|
||||||
|
last_name: data.last_name.clone(),
|
||||||
|
created_by: created_by.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(persons::table)
|
||||||
|
.values(person)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an existing person.
|
||||||
|
pub fn update_person(conn: &DbConn, id: u32, data: &PersonInsertion) -> Result<()> {
|
||||||
|
diesel::update(persons::table)
|
||||||
|
.filter(persons::id.eq(id as i64))
|
||||||
|
.set(data)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an existing person.
|
||||||
|
pub fn get_person(conn: &DbConn, id: u32) -> Result<Option<Person>> {
|
||||||
|
Ok(persons::table
|
||||||
|
.filter(persons::id.eq(id as i64))
|
||||||
|
.load::<Person>(conn)?
|
||||||
|
.first()
|
||||||
|
.cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete an existing person.
|
||||||
|
pub fn delete_person(conn: &DbConn, id: u32) -> Result<()> {
|
||||||
|
diesel::delete(persons::table.filter(persons::id.eq(id as i64))).execute(conn)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all existing persons.
|
||||||
|
pub fn get_persons(conn: &DbConn) -> Result<Vec<Person>> {
|
||||||
|
Ok(persons::table.load::<Person>(conn)?)
|
||||||
|
}
|
||||||
278
musicus_server/src/database/recordings.rs
Normal file
278
musicus_server/src/database/recordings.rs
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
use super::schema::{ensembles, instruments, performances, persons, recordings};
|
||||||
|
use super::{get_work_description, DbConn, Ensemble, Instrument, Person, WorkDescription};
|
||||||
|
use anyhow::{anyhow, Error, Result};
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::{Insertable, Queryable};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
/// A specific recording of a work.
|
||||||
|
#[derive(Insertable, Queryable, Debug, Clone)]
|
||||||
|
pub struct Recording {
|
||||||
|
pub id: i64,
|
||||||
|
pub work: i64,
|
||||||
|
pub comment: String,
|
||||||
|
pub created_by: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How a person or ensemble was involved in a recording.
|
||||||
|
#[derive(Insertable, Queryable, Debug, Clone)]
|
||||||
|
pub struct Performance {
|
||||||
|
pub id: i64,
|
||||||
|
pub recording: i64,
|
||||||
|
pub person: Option<i64>,
|
||||||
|
pub ensemble: Option<i64>,
|
||||||
|
pub role: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure for collecting all available information on a performance.
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PerformanceDescription {
|
||||||
|
pub person: Option<Person>,
|
||||||
|
pub ensemble: Option<Ensemble>,
|
||||||
|
pub role: Option<Instrument>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure for collecting all available information on a recording.
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RecordingDescription {
|
||||||
|
pub id: i64,
|
||||||
|
pub work: WorkDescription,
|
||||||
|
pub comment: String,
|
||||||
|
pub performances: Vec<PerformanceDescription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure representing data on a performance.
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PerformanceInsertion {
|
||||||
|
pub person: Option<i64>,
|
||||||
|
pub ensemble: Option<i64>,
|
||||||
|
pub role: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bundle of everything needed for adding a new recording to the database.
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RecordingInsertion {
|
||||||
|
pub work: i64,
|
||||||
|
pub comment: String,
|
||||||
|
pub performances: Vec<PerformanceInsertion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a new recording.
|
||||||
|
pub fn insert_recording(
|
||||||
|
conn: &DbConn,
|
||||||
|
id: u32,
|
||||||
|
data: &RecordingInsertion,
|
||||||
|
created_by: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
conn.transaction::<(), Error, _>(|| {
|
||||||
|
let id = id as i64;
|
||||||
|
|
||||||
|
diesel::insert_into(recordings::table)
|
||||||
|
.values(Recording {
|
||||||
|
id,
|
||||||
|
work: data.work,
|
||||||
|
comment: data.comment.clone(),
|
||||||
|
created_by: created_by.to_string(),
|
||||||
|
})
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
insert_recording_data(conn, id, data)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an existing recording.
|
||||||
|
pub fn update_recording(conn: &DbConn, id: u32, data: &RecordingInsertion) -> Result<()> {
|
||||||
|
conn.transaction::<(), Error, _>(|| {
|
||||||
|
let id = id as i64;
|
||||||
|
|
||||||
|
diesel::delete(performances::table)
|
||||||
|
.filter(performances::recording.eq(id))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
diesel::update(recordings::table)
|
||||||
|
.filter(recordings::id.eq(id))
|
||||||
|
.set((
|
||||||
|
recordings::work.eq(data.work),
|
||||||
|
recordings::comment.eq(data.comment.clone()),
|
||||||
|
))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
insert_recording_data(conn, id, data)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper method to populate other tables related to a recording.
|
||||||
|
fn insert_recording_data(conn: &DbConn, id: i64, data: &RecordingInsertion) -> Result<()> {
|
||||||
|
for performance in &data.performances {
|
||||||
|
diesel::insert_into(performances::table)
|
||||||
|
.values(Performance {
|
||||||
|
id: rand::random(),
|
||||||
|
recording: id,
|
||||||
|
person: performance.person,
|
||||||
|
ensemble: performance.ensemble,
|
||||||
|
role: performance.role,
|
||||||
|
})
|
||||||
|
.execute(conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an existing recording.
|
||||||
|
pub fn get_recording(conn: &DbConn, id: u32) -> Result<Option<Recording>> {
|
||||||
|
Ok(recordings::table
|
||||||
|
.filter(recordings::id.eq(id as i64))
|
||||||
|
.load::<Recording>(conn)?
|
||||||
|
.first()
|
||||||
|
.cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve all available information on a recording from related tables.
|
||||||
|
pub fn get_description_for_recording(
|
||||||
|
conn: &DbConn,
|
||||||
|
recording: &Recording,
|
||||||
|
) -> Result<RecordingDescription> {
|
||||||
|
let mut performance_descriptions: Vec<PerformanceDescription> = Vec::new();
|
||||||
|
|
||||||
|
let performances = performances::table
|
||||||
|
.filter(performances::recording.eq(recording.id))
|
||||||
|
.load::<Performance>(conn)?;
|
||||||
|
|
||||||
|
for performance in performances {
|
||||||
|
performance_descriptions.push(PerformanceDescription {
|
||||||
|
person: match performance.person {
|
||||||
|
Some(id) => Some(
|
||||||
|
persons::table
|
||||||
|
.filter(persons::id.eq(id as i64))
|
||||||
|
.load::<Person>(conn)?
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or(anyhow!("No person with ID: {}", id))?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
ensemble: match performance.ensemble {
|
||||||
|
Some(id) => Some(
|
||||||
|
ensembles::table
|
||||||
|
.filter(ensembles::id.eq(id as i64))
|
||||||
|
.load::<Ensemble>(conn)?
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or(anyhow!("No ensemble with ID: {}", id))?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
role: match performance.role {
|
||||||
|
Some(id) => Some(
|
||||||
|
instruments::table
|
||||||
|
.filter(instruments::id.eq(id as i64))
|
||||||
|
.load::<Instrument>(conn)?
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or(anyhow!("No instrument with ID: {}", id))?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let work_id = recording.work.try_into()?;
|
||||||
|
let work =
|
||||||
|
get_work_description(conn, work_id)?.ok_or(anyhow!("Work doesn't exist: {}", work_id))?;
|
||||||
|
|
||||||
|
let recording_description = RecordingDescription {
|
||||||
|
id: recording.id,
|
||||||
|
work,
|
||||||
|
comment: recording.comment.clone(),
|
||||||
|
performances: performance_descriptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(recording_description)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an existing recording and all available information from related tables.
|
||||||
|
pub fn get_recording_description(conn: &DbConn, id: u32) -> Result<Option<RecordingDescription>> {
|
||||||
|
let recording_description = match get_recording(conn, id)? {
|
||||||
|
Some(recording) => Some(get_description_for_recording(conn, &recording)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(recording_description)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all available information on all recordings where a person is performing.
|
||||||
|
pub fn get_recordings_for_person(
|
||||||
|
conn: &DbConn,
|
||||||
|
person_id: u32,
|
||||||
|
) -> Result<Vec<RecordingDescription>> {
|
||||||
|
let mut recording_descriptions: Vec<RecordingDescription> = Vec::new();
|
||||||
|
|
||||||
|
let recordings = 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 as i64))
|
||||||
|
.select(recordings::table::all_columns())
|
||||||
|
.load::<Recording>(conn)?;
|
||||||
|
|
||||||
|
for recording in recordings {
|
||||||
|
recording_descriptions.push(get_description_for_recording(conn, &recording)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(recording_descriptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all available information on all recordings where an ensemble is performing.
|
||||||
|
pub fn get_recordings_for_ensemble(
|
||||||
|
conn: &DbConn,
|
||||||
|
ensemble_id: u32,
|
||||||
|
) -> Result<Vec<RecordingDescription>> {
|
||||||
|
let mut recording_descriptions: Vec<RecordingDescription> = Vec::new();
|
||||||
|
|
||||||
|
let recordings = 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 as i64))
|
||||||
|
.select(recordings::table::all_columns())
|
||||||
|
.load::<Recording>(conn)?;
|
||||||
|
|
||||||
|
for recording in recordings {
|
||||||
|
recording_descriptions.push(get_description_for_recording(conn, &recording)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(recording_descriptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get allavailable information on all recordings of a work.
|
||||||
|
pub fn get_recordings_for_work(conn: &DbConn, work_id: u32) -> Result<Vec<RecordingDescription>> {
|
||||||
|
let mut recording_descriptions: Vec<RecordingDescription> = Vec::new();
|
||||||
|
|
||||||
|
let recordings = recordings::table
|
||||||
|
.filter(recordings::work.eq(work_id as i64))
|
||||||
|
.load::<Recording>(conn)?;
|
||||||
|
|
||||||
|
for recording in recordings {
|
||||||
|
recording_descriptions.push(get_description_for_recording(conn, &recording)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(recording_descriptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
pub fn delete_recording(conn: &DbConn, id: u32) -> Result<()> {
|
||||||
|
diesel::delete(recordings::table.filter(recordings::id.eq(id as i64))).execute(conn)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
120
musicus_server/src/database/schema.rs
Normal file
120
musicus_server/src/database/schema.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
table! {
|
||||||
|
ensembles (id) {
|
||||||
|
id -> Int8,
|
||||||
|
name -> Text,
|
||||||
|
created_by -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
instrumentations (id) {
|
||||||
|
id -> Int8,
|
||||||
|
work -> Int8,
|
||||||
|
instrument -> Int8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
instruments (id) {
|
||||||
|
id -> Int8,
|
||||||
|
name -> Text,
|
||||||
|
created_by -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
performances (id) {
|
||||||
|
id -> Int8,
|
||||||
|
recording -> Int8,
|
||||||
|
person -> Nullable<Int8>,
|
||||||
|
ensemble -> Nullable<Int8>,
|
||||||
|
role -> Nullable<Int8>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
persons (id) {
|
||||||
|
id -> Int8,
|
||||||
|
first_name -> Text,
|
||||||
|
last_name -> Text,
|
||||||
|
created_by -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
recordings (id) {
|
||||||
|
id -> Int8,
|
||||||
|
work -> Int8,
|
||||||
|
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 -> Int8,
|
||||||
|
part_index -> Int8,
|
||||||
|
title -> Text,
|
||||||
|
composer -> Nullable<Int8>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
work_sections (id) {
|
||||||
|
id -> Int8,
|
||||||
|
work -> Int8,
|
||||||
|
title -> Text,
|
||||||
|
before_index -> Int8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
works (id) {
|
||||||
|
id -> Int8,
|
||||||
|
composer -> Int8,
|
||||||
|
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,
|
||||||
|
);
|
||||||
61
musicus_server/src/database/users.rs
Normal file
61
musicus_server/src/database/users.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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())
|
||||||
|
}
|
||||||
307
musicus_server/src/database/works.rs
Normal file
307
musicus_server/src/database/works.rs
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
use super::schema::{instrumentations, instruments, persons, work_parts, work_sections, works};
|
||||||
|
use super::{get_person, DbConn, Instrument, Person};
|
||||||
|
use anyhow::{anyhow, Error, Result};
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::{Insertable, Queryable};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
/// A composition by a composer.
|
||||||
|
#[derive(Insertable, Queryable, Debug, Clone)]
|
||||||
|
pub struct Work {
|
||||||
|
pub id: i64,
|
||||||
|
pub composer: i64,
|
||||||
|
pub title: String,
|
||||||
|
pub created_by: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Definition that a work uses an instrument.
|
||||||
|
#[derive(Insertable, Queryable, Debug, Clone)]
|
||||||
|
pub struct Instrumentation {
|
||||||
|
pub id: i64,
|
||||||
|
pub work: i64,
|
||||||
|
pub instrument: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A concrete work part that can be recorded.
|
||||||
|
#[derive(Insertable, Queryable, Debug, Clone)]
|
||||||
|
pub struct WorkPart {
|
||||||
|
pub id: i64,
|
||||||
|
pub work: i64,
|
||||||
|
pub part_index: i64,
|
||||||
|
pub title: String,
|
||||||
|
pub composer: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A heading between work parts.
|
||||||
|
#[derive(Insertable, Queryable, Debug, Clone)]
|
||||||
|
pub struct WorkSection {
|
||||||
|
pub id: i64,
|
||||||
|
pub work: i64,
|
||||||
|
pub title: String,
|
||||||
|
pub before_index: i64,
|
||||||
|
}
|
||||||
|
/// A structure for collecting all available information on a work part.
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WorkPartDescription {
|
||||||
|
pub title: String,
|
||||||
|
pub composer: Option<Person>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure for collecting all available information on a work section.
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WorkSectionDescription {
|
||||||
|
pub title: String,
|
||||||
|
pub before_index: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure for collecting all available information on a work.
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WorkDescription {
|
||||||
|
pub id: i64,
|
||||||
|
pub title: String,
|
||||||
|
pub composer: Person,
|
||||||
|
pub instruments: Vec<Instrument>,
|
||||||
|
pub parts: Vec<WorkPartDescription>,
|
||||||
|
pub sections: Vec<WorkSectionDescription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure representing data on a work part.
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WorkPartInsertion {
|
||||||
|
pub title: String,
|
||||||
|
pub composer: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure representing data on a work section.
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WorkSectionInsertion {
|
||||||
|
pub title: String,
|
||||||
|
pub before_index: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A structure representing data on a work.
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WorkInsertion {
|
||||||
|
pub composer: i64,
|
||||||
|
pub title: String,
|
||||||
|
pub instruments: Vec<i64>,
|
||||||
|
pub parts: Vec<WorkPartInsertion>,
|
||||||
|
pub sections: Vec<WorkSectionInsertion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a new work.
|
||||||
|
pub fn insert_work(conn: &DbConn, id: u32, data: &WorkInsertion, created_by: &str) -> Result<()> {
|
||||||
|
conn.transaction::<(), Error, _>(|| {
|
||||||
|
let id = id as i64;
|
||||||
|
|
||||||
|
diesel::insert_into(works::table)
|
||||||
|
.values(Work {
|
||||||
|
id,
|
||||||
|
composer: data.composer.clone(),
|
||||||
|
title: data.title.clone(),
|
||||||
|
created_by: created_by.to_string(),
|
||||||
|
})
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
insert_work_data(conn, id, data)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an existing work.
|
||||||
|
pub fn update_work(conn: &DbConn, id: u32, data: &WorkInsertion) -> Result<()> {
|
||||||
|
conn.transaction::<(), Error, _>(|| {
|
||||||
|
let id = id as i64;
|
||||||
|
|
||||||
|
diesel::delete(instrumentations::table)
|
||||||
|
.filter(instrumentations::work.eq(id))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
diesel::delete(work_parts::table)
|
||||||
|
.filter(work_parts::work.eq(id))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
diesel::delete(work_sections::table)
|
||||||
|
.filter(work_sections::work.eq(id))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
diesel::update(works::table)
|
||||||
|
.filter(works::id.eq(id))
|
||||||
|
.set((
|
||||||
|
works::composer.eq(data.composer),
|
||||||
|
works::title.eq(data.title.clone()),
|
||||||
|
))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
insert_work_data(conn, id, data)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper method to populate tables related to a work.
|
||||||
|
fn insert_work_data(conn: &DbConn, id: i64, data: &WorkInsertion) -> Result<()> {
|
||||||
|
for instrument in &data.instruments {
|
||||||
|
diesel::insert_into(instrumentations::table)
|
||||||
|
.values(Instrumentation {
|
||||||
|
id: rand::random(),
|
||||||
|
work: id,
|
||||||
|
instrument: *instrument,
|
||||||
|
})
|
||||||
|
.execute(conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, part) in data.parts.iter().enumerate() {
|
||||||
|
let part = WorkPart {
|
||||||
|
id: rand::random(),
|
||||||
|
work: id,
|
||||||
|
part_index: index.try_into()?,
|
||||||
|
title: part.title.clone(),
|
||||||
|
composer: part.composer,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(work_parts::table)
|
||||||
|
.values(part)
|
||||||
|
.execute(conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for section in &data.sections {
|
||||||
|
let section = WorkSection {
|
||||||
|
id: rand::random(),
|
||||||
|
work: id,
|
||||||
|
title: section.title.clone(),
|
||||||
|
before_index: section.before_index,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(work_sections::table)
|
||||||
|
.values(section)
|
||||||
|
.execute(conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an already existing work without related rows from other tables.
|
||||||
|
fn get_work(conn: &DbConn, id: u32) -> Result<Option<Work>> {
|
||||||
|
Ok(works::table
|
||||||
|
.filter(works::id.eq(id as i64))
|
||||||
|
.load::<Work>(conn)?
|
||||||
|
.first()
|
||||||
|
.cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve all available information on a work from related tables.
|
||||||
|
fn get_description_for_work(conn: &DbConn, work: &Work) -> Result<WorkDescription> {
|
||||||
|
let mut instruments: Vec<Instrument> = Vec::new();
|
||||||
|
|
||||||
|
let instrumentations = instrumentations::table
|
||||||
|
.filter(instrumentations::work.eq(work.id))
|
||||||
|
.load::<Instrumentation>(conn)?;
|
||||||
|
|
||||||
|
for instrumentation in instrumentations {
|
||||||
|
instruments.push(
|
||||||
|
instruments::table
|
||||||
|
.filter(instruments::id.eq(instrumentation.instrument))
|
||||||
|
.load::<Instrument>(conn)?
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or(anyhow!(
|
||||||
|
"No instrument with ID: {}",
|
||||||
|
instrumentation.instrument
|
||||||
|
))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut part_descriptions: Vec<WorkPartDescription> = Vec::new();
|
||||||
|
|
||||||
|
let work_parts = work_parts::table
|
||||||
|
.filter(work_parts::work.eq(work.id))
|
||||||
|
.load::<WorkPart>(conn)?;
|
||||||
|
|
||||||
|
for work_part in work_parts {
|
||||||
|
part_descriptions.push(WorkPartDescription {
|
||||||
|
title: work_part.title,
|
||||||
|
composer: match work_part.composer {
|
||||||
|
Some(composer) => Some(
|
||||||
|
persons::table
|
||||||
|
.filter(persons::id.eq(composer))
|
||||||
|
.load::<Person>(conn)?
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or(anyhow!("No person with ID: {}", composer))?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut section_descriptions: Vec<WorkSectionDescription> = Vec::new();
|
||||||
|
|
||||||
|
let sections = work_sections::table
|
||||||
|
.filter(work_sections::work.eq(work.id))
|
||||||
|
.load::<WorkSection>(conn)?;
|
||||||
|
|
||||||
|
for section in sections {
|
||||||
|
section_descriptions.push(WorkSectionDescription {
|
||||||
|
title: section.title,
|
||||||
|
before_index: section.before_index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let person_id = work.composer.try_into()?;
|
||||||
|
let person =
|
||||||
|
get_person(conn, person_id)?.ok_or(anyhow!("Person doesn't exist: {}", person_id))?;
|
||||||
|
|
||||||
|
Ok(WorkDescription {
|
||||||
|
id: work.id,
|
||||||
|
composer: person,
|
||||||
|
title: work.title.clone(),
|
||||||
|
instruments,
|
||||||
|
parts: part_descriptions,
|
||||||
|
sections: section_descriptions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an existing work and all available information from related tables.
|
||||||
|
pub fn get_work_description(conn: &DbConn, id: u32) -> Result<Option<WorkDescription>> {
|
||||||
|
let work_description = match get_work(conn, id)? {
|
||||||
|
Some(work) => Some(get_description_for_work(conn, &work)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(work_description)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 it
|
||||||
|
pub fn delete_work(conn: &DbConn, id: u32) -> Result<()> {
|
||||||
|
diesel::delete(works::table.filter(works::id.eq(id as i64))).execute(conn)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all existing works by a composer and related information from other tables.
|
||||||
|
pub fn get_work_descriptions(conn: &DbConn, composer_id: u32) -> Result<Vec<WorkDescription>> {
|
||||||
|
let mut work_descriptions: Vec<WorkDescription> = Vec::new();
|
||||||
|
|
||||||
|
let works = works::table
|
||||||
|
.filter(works::composer.eq(composer_id as i64))
|
||||||
|
.load::<Work>(conn)?;
|
||||||
|
|
||||||
|
for work in works {
|
||||||
|
work_descriptions.push(get_description_for_work(conn, &work)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(work_descriptions)
|
||||||
|
}
|
||||||
36
musicus_server/src/main.rs
Normal file
36
musicus_server/src/main.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Required for database/schema.rs
|
||||||
|
#[macro_use]
|
||||||
|
extern crate diesel;
|
||||||
|
|
||||||
|
use actix_web::{App, HttpServer};
|
||||||
|
|
||||||
|
mod database;
|
||||||
|
|
||||||
|
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(post_person)
|
||||||
|
.service(put_person)
|
||||||
|
.service(get_persons)
|
||||||
|
});
|
||||||
|
|
||||||
|
server.bind("127.0.0.1:8087")?.run().await
|
||||||
|
}
|
||||||
261
musicus_server/src/routes/auth.rs
Normal file
261
musicus_server/src/routes/auth.rs
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
use super::ServerError;
|
||||||
|
use crate::database;
|
||||||
|
use crate::database::{DbConn, DbPool, User, UserInsertion};
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a token allows the user to create a new item.
|
||||||
|
pub fn may_create(conn: &DbConn, token: &str) -> Result<bool> {
|
||||||
|
let user = authenticate(conn, token)?;
|
||||||
|
|
||||||
|
let result = if user.is_banned { false } else { false };
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a token allows the user to edit an item created by him or somebody else.
|
||||||
|
pub fn may_edit(conn: &DbConn, token: &str, created_by: &str) -> Result<bool> {
|
||||||
|
let user = authenticate(conn, token)?;
|
||||||
|
|
||||||
|
let result = if user.is_banned {
|
||||||
|
false
|
||||||
|
} else if user.username == created_by {
|
||||||
|
true
|
||||||
|
} else if user.is_editor {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
0
musicus_server/src/routes/ensembles.rs
Normal file
0
musicus_server/src/routes/ensembles.rs
Normal file
47
musicus_server/src/routes/error.rs
Normal file
47
musicus_server/src/routes/error.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
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(error: r2d2::Error) -> Self {
|
||||||
|
ServerError::Internal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<anyhow::Error> for ServerError {
|
||||||
|
fn from(error: anyhow::Error) -> Self {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
musicus_server/src/routes/instruments.rs
Normal file
0
musicus_server/src/routes/instruments.rs
Normal file
20
musicus_server/src/routes/mod.rs
Normal file
20
musicus_server/src/routes/mod.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
pub mod auth;
|
||||||
|
pub use auth::*;
|
||||||
|
|
||||||
|
pub mod ensembles;
|
||||||
|
pub use ensembles::*;
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub use error::*;
|
||||||
|
|
||||||
|
pub mod instruments;
|
||||||
|
pub use instruments::*;
|
||||||
|
|
||||||
|
pub mod persons;
|
||||||
|
pub use persons::*;
|
||||||
|
|
||||||
|
pub mod recordings;
|
||||||
|
pub use recordings::*;
|
||||||
|
|
||||||
|
pub mod works;
|
||||||
|
pub use works::*;
|
||||||
103
musicus_server/src/routes/persons.rs
Normal file
103
musicus_server/src/routes/persons.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
use super::{authenticate, ServerError};
|
||||||
|
use crate::database;
|
||||||
|
use crate::database::{DbPool, PersonInsertion};
|
||||||
|
use actix_web::{delete, get, post, put, 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<u32>,
|
||||||
|
) -> Result<HttpResponse, ServerError> {
|
||||||
|
let person = 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(person))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new person. The user must be authorized to do that.
|
||||||
|
#[post("/persons")]
|
||||||
|
pub async fn post_person(
|
||||||
|
auth: BearerAuth,
|
||||||
|
db: web::Data<DbPool>,
|
||||||
|
data: web::Json<PersonInsertion>,
|
||||||
|
) -> Result<HttpResponse, ServerError> {
|
||||||
|
let id = rand::random();
|
||||||
|
|
||||||
|
web::block(move || {
|
||||||
|
let conn = db.into_inner().get()?;
|
||||||
|
let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?;
|
||||||
|
|
||||||
|
database::insert_person(&conn, id, &data.into_inner(), &user.username)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().body(id.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[put("/persons/{id}")]
|
||||||
|
pub async fn put_person(
|
||||||
|
auth: BearerAuth,
|
||||||
|
db: web::Data<DbPool>,
|
||||||
|
id: web::Path<u32>,
|
||||||
|
data: web::Json<PersonInsertion>,
|
||||||
|
) -> Result<HttpResponse, ServerError> {
|
||||||
|
web::block(move || {
|
||||||
|
let conn = db.into_inner().get()?;
|
||||||
|
|
||||||
|
let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?;
|
||||||
|
|
||||||
|
let id = id.into_inner();
|
||||||
|
let old_person = database::get_person(&conn, id)?.ok_or(ServerError::NotFound)?;
|
||||||
|
|
||||||
|
if user.username != old_person.created_by {
|
||||||
|
Err(ServerError::Forbidden)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
database::update_person(&conn, id, &data.into_inner())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/persons")]
|
||||||
|
pub async fn get_persons(db: web::Data<DbPool>) -> Result<HttpResponse, ServerError> {
|
||||||
|
let persons = web::block(move || {
|
||||||
|
let conn = db.into_inner().get()?;
|
||||||
|
Ok(database::get_persons(&conn)?)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(persons))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("/persons/{id}")]
|
||||||
|
pub async fn delete_person(
|
||||||
|
auth: BearerAuth,
|
||||||
|
db: web::Data<DbPool>,
|
||||||
|
id: web::Path<u32>,
|
||||||
|
) -> Result<HttpResponse, ServerError> {
|
||||||
|
web::block(move || {
|
||||||
|
let conn = db.into_inner().get()?;
|
||||||
|
let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?;
|
||||||
|
|
||||||
|
if user.is_editor {
|
||||||
|
database::delete_person(&conn, id.into_inner())?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ServerError::Forbidden)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().finish())
|
||||||
|
}
|
||||||
0
musicus_server/src/routes/recordings.rs
Normal file
0
musicus_server/src/routes/recordings.rs
Normal file
0
musicus_server/src/routes/works.rs
Normal file
0
musicus_server/src/routes/works.rs
Normal file
Loading…
Add table
Add a link
Reference in a new issue