diff --git a/README.md b/README.md index a22d423..18ecd82 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,6 @@ depend on other ones. All packages are written in [Dart](https://dart.dev). `database` – A Database of classical music. This package will be used by all standalone Musicus applications for storing classical music metadata. -`server` – A simple http server hosting a Musicus database. The server is -developed using the [Aqueduct framework](https://aqueduct.io). - `client` – A client library for the Musicus server. `common` – Common building blocks for Musicus client apps. This includes shared diff --git a/database/.gitignore b/database/.gitignore deleted file mode 100644 index a242fcb..0000000 --- a/database/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# VS Code related -.vscode/ - -# Flutter/Dart/Pub related -**/*.g.dart -**/doc/api/ -.dart_tool/ -pubspec.lock -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ diff --git a/database/build.yaml b/database/build.yaml deleted file mode 100644 index 5c3643e..0000000 --- a/database/build.yaml +++ /dev/null @@ -1,7 +0,0 @@ -targets: - $default: - builders: - moor_generator: - options: - generate_connect_constructor: true - use_column_name_as_json_key_when_defined_in_moor_file: false \ No newline at end of file diff --git a/database/lib/musicus_database.dart b/database/lib/musicus_database.dart deleted file mode 100644 index de7af64..0000000 --- a/database/lib/musicus_database.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'src/database.dart'; -export 'src/info.dart'; \ No newline at end of file diff --git a/database/lib/src/database.dart b/database/lib/src/database.dart deleted file mode 100644 index 9fbb21d..0000000 --- a/database/lib/src/database.dart +++ /dev/null @@ -1,337 +0,0 @@ -import 'dart:math'; - -import 'package:moor/moor.dart'; - -import 'info.dart'; - -part 'database.g.dart'; - -final _random = Random(DateTime.now().millisecondsSinceEpoch); -int generateId() => _random.nextInt(0xFFFFFFFF); - -@UseMoor( - include: { - 'database.moor', - }, -) -class Database extends _$Database { - static const pageSize = 25; - - Database(QueryExecutor queryExecutor) : super(queryExecutor); - - Database.connect(DatabaseConnection connection) : super.connect(connection); - - @override - int get schemaVersion => 1; - - @override - MigrationStrategy get migration => MigrationStrategy( - beforeOpen: (details) async { - await customStatement('PRAGMA foreign_keys = ON'); - }, - ); - - /// Get all available persons. - /// - /// This will return a list of [pageSize] persons. You can get another page - /// using the [page] parameter. If a non empty [search] string is provided, - /// the persons will get filtered based on that string. - Future> getPersons([int page, String search]) async { - final offset = page != null ? page * pageSize : 0; - List result; - - if (search == null || search.isEmpty) { - result = await allPersons(pageSize, offset).get(); - } else { - result = await searchPersons('$search%', pageSize, offset).get(); - } - - return result; - } - - /// Add [person] or replace an existing person with the same ID. - Future updatePerson(Person person) async { - await into(persons).insert( - person, - mode: InsertMode.insertOrReplace, - ); - } - - /// Delete the person by [id]. - Future deletePerson(int id) async { - await (delete(persons)..where((p) => p.id.equals(id))).go(); - } - - /// Get all available instruments. - /// - /// This will return a list of [pageSize] instruments. You can get another - /// page using the [page] parameter. If a non empty [search] string is - /// provided, the instruments will get filtered based on that string. - Future> getInstruments([int page, String search]) async { - final offset = page != null ? page * pageSize : 0; - List result; - - if (search == null || search.isEmpty) { - result = await allInstruments(pageSize, offset).get(); - } else { - result = await searchInstruments('$search%', pageSize, offset).get(); - } - - return result; - } - - /// Add [instrument] or replace an existing one with the same ID. - Future updateInstrument(Instrument instrument) async { - await into(instruments).insert( - instrument, - mode: InsertMode.insertOrReplace, - ); - } - - /// Delete the instrument by [id]. - Future deleteInstrument(int id) async { - await (delete(instruments)..where((i) => i.id.equals(id))).go(); - } - - /// Retrieve more information on an already queried work. - Future getWorkInfo(Work work) async { - final id = work.id; - - final composers = await partComposersByWork(id).get(); - composers.insert(0, await personById(work.composer).getSingle()); - final instruments = await instrumentsByWork(id).get(); - - final List parts = []; - for (final part in await partsByWork(id).get()) { - parts.add(PartInfo( - part: part, - composer: part.composer != null - ? await personById(part.composer).getSingle() - : null, - instruments: await instrumentsByWorkPart(part.id).get(), - )); - } - - final List sections = []; - for (final section in await sectionsByWork(id).get()) { - sections.add(section); - } - - return WorkInfo( - work: work, - instruments: instruments, - composers: composers, - parts: parts, - sections: sections, - ); - } - - /// Get all available information on a work. - Future getWork(int id) async { - final work = await workById(id).getSingle(); - return await getWorkInfo(work); - } - - /// Get information on all works written by the person with ID [personId]. - /// - /// This will return a list of [pageSize] results. You can get another page - /// using the [page] parameter. If a non empty [search] string is provided, - /// the works will be filtered using that string. - Future> getWorks(int personId, - [int page, String search]) async { - final offset = page != null ? page * pageSize : 0; - List works; - - if (search == null || search.isEmpty) { - works = await worksByComposer(personId, pageSize, offset).get(); - } else { - works = - await searchWorksByComposer(personId, '$search%', pageSize, offset) - .get(); - } - - final List result = []; - for (final work in works) { - result.add(await getWorkInfo(work)); - } - - return result; - } - - /// Add or replace a work and its associated data. - /// - /// This will explicitly update all associated composers and instruments, even - /// if they have already existed before. - Future updateWork(WorkInfo workInfo) async { - await transaction(() async { - final workId = workInfo.work.id; - - // Delete old work data first. The parts, sections and instrumentations - // will be deleted automatically due to their foreign key constraints. - await deleteWork(workId); - - // This will also include the composers of the work's parts. - for (final person in workInfo.composers) { - await updatePerson(person); - } - - await into(works).insert(workInfo.work); - - // At the moment, this will also update all provided instruments, even if - // they were already there previously. - for (final instrument in workInfo.instruments) { - await updateInstrument(instrument); - await into(instrumentations).insert(Instrumentation( - work: workId, - instrument: instrument.id, - )); - } - - for (final partInfo in workInfo.parts) { - final part = partInfo.part; - - await into(workParts).insert(part); - - for (final instrument in workInfo.instruments) { - await updateInstrument(instrument); - await into(partInstrumentations).insert(PartInstrumentation( - workPart: part.id, - instrument: instrument.id, - )); - } - } - - for (final section in workInfo.sections) { - await into(workSections).insert(section); - } - }); - } - - /// Delete the work by [id]. - Future deleteWork(int id) async { - // The parts and instrumentations will be deleted automatically due to - // their foreign key constraints. - await (delete(works)..where((w) => w.id.equals(id))).go(); - } - - /// Get all available ensembles. - /// - /// This will return a list of [pageSize] ensembles. You can get another page - /// using the [page] parameter. If a non empty [search] string is provided, - /// the ensembles will get filtered based on that string. - Future> getEnsembles([int page, String search]) async { - final offset = page != null ? page * pageSize : 0; - List result; - - if (search == null || search.isEmpty) { - result = await allEnsembles(pageSize, offset).get(); - } else { - result = await searchEnsembles('$search%', pageSize, offset).get(); - } - - return result; - } - - /// Add [ensemble] or replace an existing one with the same ID. - Future updateEnsemble(Ensemble ensemble) async { - await into(ensembles).insert( - ensemble, - mode: InsertMode.insertOrReplace, - ); - } - - /// Delete the ensemble by [id]. - Future deleteEnsemble(int id) async { - await (delete(ensembles)..where((e) => e.id.equals(id))).go(); - } - - /// Add or replace a recording and its associated data. - /// - /// This will explicitly also update all assoicated persons and instruments. - Future updateRecording(RecordingInfo recordingInfo) async { - await transaction(() async { - final recordingId = recordingInfo.recording.id; - - // Delete the old recording first. This will also delete the performances - // due to their foreign key constraint. - await deleteRecording(recordingId); - - await into(recordings).insert(recordingInfo.recording); - - for (final performance in recordingInfo.performances) { - if (performance.person != null) { - await updatePerson(performance.person); - } - - if (performance.ensemble != null) { - await updateEnsemble(performance.ensemble); - } - - if (performance.role != null) { - await updateInstrument(performance.role); - } - - await into(performances).insert(Performance( - recording: recordingId, - person: performance.person?.id, - ensemble: performance.ensemble?.id, - role: performance.role?.id, - )); - } - }); - } - - /// Retreive more information on an already queried recording. - Future getRecordingInfo(Recording recording) async { - final id = recording.id; - - final List performances = []; - for (final performance in await performancesByRecording(id).get()) { - performances.add(PerformanceInfo( - person: performance.person != null - ? await personById(performance.person).getSingle() - : null, - ensemble: performance.ensemble != null - ? await ensembleById(performance.ensemble).getSingle() - : null, - role: performance.role != null - ? await instrumentById(performance.role).getSingle() - : null, - )); - } - - return RecordingInfo( - recording: recording, - performances: performances, - ); - } - - /// Get all available information on a recording. - Future getRecording(int id) async { - final recording = await recordingById(id).getSingle(); - return await getRecordingInfo(recording); - } - - /// Delete a recording by [id]. - Future deleteRecording(int id) async { - // This will also delete the performances due to their foreign key - // constraint. - await (delete(recordings)..where((r) => r.id.equals(id))).go(); - } - - /// Get information on all recordings of the work with ID [workId]. - /// - /// This will return a list of [pageSize] recordings. You can get the other - /// pages using the [page] parameter. - Future> getRecordings(int workId, [int page]) async { - final offset = page != null ? page * pageSize : 0; - final recordings = await recordingsByWork(workId, pageSize, offset).get(); - - final List result = []; - for (final recording in recordings) { - result.add(await getRecordingInfo(recording)); - } - - return result; - } -} diff --git a/database/lib/src/database.moor b/database/lib/src/database.moor deleted file mode 100644 index 56affab..0000000 --- a/database/lib/src/database.moor +++ /dev/null @@ -1,139 +0,0 @@ -CREATE TABLE persons ( - id INTEGER NOT NULL PRIMARY KEY, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL -); - --- This represents real instruments as well as other roles that can be played --- in a recording. -CREATE TABLE instruments ( - id INTEGER NOT NULL PRIMARY KEY, - name TEXT NOT NULL -); - -CREATE TABLE works ( - id INTEGER NOT NULL PRIMARY KEY, - composer INTEGER REFERENCES persons(id) ON DELETE SET NULL, - title TEXT NOT NULL -); - -CREATE TABLE instrumentations ( - work INTEGER NOT NULL REFERENCES works(id) ON DELETE CASCADE, - instrument INTEGER NOT NULL REFERENCES instruments(id) ON DELETE CASCADE -); - -CREATE TABLE work_parts ( - id INTEGER NOT NULL PRIMARY KEY, - composer INTEGER REFERENCES persons(id) ON DELETE SET NULL, - title TEXT NOT NULL, - part_of INTEGER NOT NULL REFERENCES works(id) ON DELETE CASCADE, - part_index INTEGER NOT NULL -); - -CREATE TABLE part_instrumentations ( - work_part INTEGER NOT NULL REFERENCES works(id) ON DELETE CASCADE, - instrument INTEGER NOT NULL REFERENCES instruments(id) ON DELETE CASCADE -); - -CREATE TABLE work_sections ( - id INTEGER NOT NULL PRIMARY KEY, - work INTEGER NOT NULL REFERENCES works(id) ON DELETE CASCADE, - title TEXT NOT NULL, - before_part_index INTEGER NOT NULL -); - -CREATE TABLE ensembles ( - id INTEGER NOT NULL PRIMARY KEY, - name TEXT NOT NULL -); - -CREATE TABLE recordings ( - id INTEGER NOT NULL PRIMARY KEY, - work INTEGER REFERENCES works(id) ON DELETE SET NULL, - comment TEXT NOT NULL -); - -CREATE TABLE performances ( - recording INTEGER NOT NULL REFERENCES recordings(id) ON DELETE CASCADE, - person INTEGER REFERENCES persons(id) ON DELETE CASCADE, - ensemble INTEGER REFERENCES ensembles(id) ON DELETE CASCADE, - role INTEGER REFERENCES instruments(id) ON DELETE SET NULL -); - -allPersons: -SELECT * FROM persons ORDER BY last_name, first_name - LIMIT :limit OFFSET :offset; - -searchPersons: -SELECT * FROM persons WHERE last_name LIKE :search - ORDER BY last_name, first_name LIMIT :limit OFFSET :offset; - -personById: -SELECT * FROM persons WHERE id = :id LIMIT 1; - -allInstruments: -SELECT * FROM instruments ORDER BY name LIMIT :limit OFFSET :offset; - -searchInstruments: -SELECT * FROM instruments WHERE name LIKE :search ORDER BY name - LIMIT :limit OFFSET :offset; - -instrumentById: -SELECT * FROM instruments WHERE id = :id LIMIT 1; - -workById: -SELECT * FROM works WHERE id = :id LIMIT 1; - -partsByWork: -SELECT * FROM work_parts WHERE part_of = :id ORDER BY part_index; - -sectionsByWork: -SELECT * FROM work_sections WHERE work = :id ORDER BY before_part_index; - -worksByComposer: -SELECT DISTINCT works.* FROM works - JOIN work_parts ON work_parts.part_of = works.id - WHERE works.composer = :id OR work_parts.composer = :id - ORDER BY works.title LIMIT :limit OFFSET :offset; - -searchWorksByComposer: -SELECT DISTINCT works.* FROM works - JOIN work_parts ON work_parts.part_of = works.id - WHERE (works.composer = :id OR work_parts.composer = :id) - AND works.title LIKE :search - ORDER BY works.title LIMIT :limit OFFSET :offset; - -partComposersByWork: -SELECT DISTINCT persons.* FROM persons - JOIN work_parts ON work_parts.composer = persons.id - WHERE work_parts.part_of = :id; - -instrumentsByWork: -SELECT instruments.* FROM instrumentations - JOIN instruments ON instrumentations.instrument = instruments.id - WHERE instrumentations.work = :workId; - -instrumentsByWorkPart: -SELECT instruments.* FROM part_instrumentations - JOIN instruments ON part_instrumentations.instrument = instruments.id - WHERE part_instrumentations.work_part = :id; - -allEnsembles: -SELECT * FROM ensembles ORDER BY name LIMIT :limit OFFSET :offset; - -searchEnsembles: -SELECT * FROM ensembles WHERE name LIKE :search ORDER BY name - LIMIT :limit OFFSET :offset; - -ensembleById: -SELECT * FROM ensembles WHERE id = :id LIMIT 1; - -recordingById: -SELECT * FROM recordings WHERE id = :id; - -recordingsByWork: -SELECT * FROM recordings WHERE work = :id ORDER BY id - LIMIT :limit OFFSET :offset; - -performancesByRecording: -SELECT * FROM performances WHERE recording = :id; \ No newline at end of file diff --git a/database/lib/src/info.dart b/database/lib/src/info.dart deleted file mode 100644 index d337b3d..0000000 --- a/database/lib/src/info.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'database.dart'; - -/// A bundle of all available information on a work part. -class PartInfo { - /// The work part itself. - final WorkPart part; - - /// A list of instruments. - /// - /// This will include the instruments, that are specific to this part. - final List instruments; - - /// The composer of this part. - /// - /// This is null, if this part doesn't have a specific composer. - final Person composer; - - PartInfo({ - this.part, - this.instruments, - this.composer, - }); - - factory PartInfo.fromJson(Map json) => PartInfo( - part: WorkPart.fromJson(json['part']), - instruments: json['instruments'] - .map((j) => Instrument.fromJson(j)) - .toList(), - composer: - json['composer'] != null ? Person.fromJson(json['composer']) : null, - ); - - Map toJson() => { - 'part': part.toJson(), - 'instruments': instruments.map((i) => i.toJson()).toList(), - 'composers': composer?.toJson(), - }; -} - -/// A bundle information on a work. -/// -/// This includes all available information except for recordings of this work. -class WorkInfo { - /// The work itself. - final Work work; - - /// A list of instruments. - /// - /// This will not the include the instruments, that are specific to the work - /// parts. - final List instruments; - - /// A list of persons, which will include all part composers. - final List composers; - - /// All available information on the work parts. - final List parts; - - /// The sections of this work. - final List sections; - - WorkInfo({ - this.work, - this.instruments, - this.composers, - this.parts, - this.sections, - }); - - factory WorkInfo.fromJson(Map json) => WorkInfo( - work: Work.fromJson(json['work']), - instruments: json['instruments'] - .map((j) => Instrument.fromJson(j)) - .toList(), - composers: - json['composers'].map((j) => Person.fromJson(j)).toList(), - parts: - json['parts'].map((j) => PartInfo.fromJson(j)).toList(), - sections: json['sections'] - .map((j) => WorkSection.fromJson(j)) - .toList(), - ); - - Map toJson() => { - 'work': work.toJson(), - 'instruments': instruments.map((i) => i.toJson()).toList(), - 'composers': composers.map((c) => c.toJson()).toList(), - 'parts': parts.map((c) => c.toJson()).toList(), - 'sections': sections.map((s) => s.toJson()).toList(), - }; -} - -/// All available information on a performance within a recording. -class PerformanceInfo { - /// The performing person. - /// - /// This will be null, if this is an ensemble. - final Person person; - - /// The performing ensemble. - /// - /// This will be null, if this is a person. - final Ensemble ensemble; - - /// The instrument/role or null. - final Instrument role; - - PerformanceInfo({ - this.person, - this.ensemble, - this.role, - }); - - factory PerformanceInfo.fromJson(Map json) => - PerformanceInfo( - person: json['person'] != null ? Person.fromJson(json['person']) : null, - ensemble: json['ensemble'] != null - ? Ensemble.fromJson(json['ensemble']) - : null, - role: json['role'] != null ? Instrument.fromJson(json['role']) : null, - ); - - Map toJson() => { - 'person': person?.toJson(), - 'ensemble': ensemble?.toJson(), - 'role': role?.toJson(), - }; -} - -/// All available information on a recording. -/// -/// This doesn't include the recorded work, because probably it's already -/// available. -class RecordingInfo { - /// The recording itself. - final Recording recording; - - /// Information on the performances within this recording. - final List performances; - - RecordingInfo({ - this.recording, - this.performances, - }); - - factory RecordingInfo.fromJson(Map json) => RecordingInfo( - recording: Recording.fromJson(json['recording']), - performances: json['performances'] - .map((j) => PerformanceInfo.fromJson(j)) - .toList(), - ); - - Map toJson() => { - 'recording': recording.toJson(), - 'performances': performances.map((p) => p.toJson()).toList(), - }; -} diff --git a/database/pubspec.yaml b/database/pubspec.yaml deleted file mode 100644 index 9377c4d..0000000 --- a/database/pubspec.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: musicus_database -description: A database for classical music. -version: 0.0.1 - -environment: - sdk: ">=2.3.0 <3.0.0" - -dependencies: - moor: - moor_ffi: - -dev_dependencies: - build_runner: - moor_generator: \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore deleted file mode 100644 index a242fcb..0000000 --- a/server/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# VS Code related -.vscode/ - -# Flutter/Dart/Pub related -**/*.g.dart -**/doc/api/ -.dart_tool/ -pubspec.lock -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ diff --git a/server/README.md b/server/README.md deleted file mode 100644 index a55e0f0..0000000 --- a/server/README.md +++ /dev/null @@ -1,207 +0,0 @@ -# Musicus server - -A server hosting a shared Musicus database. - -## Introduction - -A Musicus server publishes the contents of a Musicus database via a simple -HTTP API. Registered users may additionally add entities to the database and -some users maintain the database by editing or deleting entities. - -## API documentation - -Important note: The Musicus API is not stable yet. This means, that there will -probably be breaking changes without any kind of versioning. At the moment, -this documentation while mostly describing the API as it works today is -nothing more than a draft. - -### Retrieving information - -All entities are available to the public without authentication. The response -will have the content type `application/json` and the body will contain either -a list of JSON objects or just a JSON object. The server handles `GET` requests -at the following routes: - -| Route | Result | Pagination | Search | -| ------------------------ | ----------------------------------------------- | ---------- | ------ | -| `/persons` | A list of persons | Yes | Yes | -| `/persons/{id}` | One person by its ID or error `404` | No | No | -| `/persons/{id}/works` | A list of works by the person or error `404` | Yes | Yes | -| `/instruments` | A list of instruments | Yes | Yes | -| `/instruments/{id}` | One instrument by its ID or error `404` | No | No | -| `/works/{id}` | One work by its ID or error `404` | No | No | -| `/works/{id}/recordings` | A list of recordings of the work or error `404` | Yes | No | -| `/ensembles` | A list of ensembles | Yes | Yes | -| `/ensembles/{id}` | One ensemble by its ID or error `404` | No | No | -| `/recordings/{id}` | One recording by its ID or error `404` | No | No | - -#### Pagination - -Routes that use pagination for their result will always limit the result to a -constant amount of entities. You can get other pages using the `?p={page}` -query parameter. - -#### Search - -Routes supporting search can be supplied with a search string using the -`?s={search}` query parameter. - -### Authentication - -Users that would like to contribute to the information hosted by the server -will need to authenticate. - -#### Registration - -For registration, the server handles `POST` requests to `/account/register`. -The request body has to be valid JSON and have the following form. - -```json -{ - "username": "username", - "email": "optional@email.address", - "password": "password" -} -``` - -The following errors may occur: - -| Error code | Explanation | -| ---------- | ---------------------------------------- | -| `400` | The body was malformed. | -| `409` | The username is already taken. | -| `415` | Content type was not `application/json`. | - -#### Login - -All protected resources will check for a valid token within the authorization -header of the request. The client can get a token by sending a `POST` request -to `/account/login`. The request body should contain a valid JSON object of the -following form: - -```json -{ - "username": "username", - "password": "password" -} -``` - -If the operation was successful, the token will be returned in the response -body as a single string with the content type `text/plain`. - -The following errors may occur: - -| Error code | Explanation | -| ---------- | ---------------------------------------- | -| `400` | The body was malformed. | -| `401` | Login failed | -| `415` | Content type was not `application/json`. | - -#### Authorization - -When accessing a protected resource, the client should include a authorization -header with the token retrieved when logging in. The authorization type should -be `Bearer`. If the provided token is valid and the user is authorized to -perform the requested action, the expected response for the route beeing -accessed will be returned. - -The following errors may occur: - -| Error code | Explanation | -| ---------- | -------------------------------------------------------- | -| `400` | The authorization header was malformed. | -| `401` | The provided token is invalid. | -| `403` | The user is not allowed to perform the requested action. | - -#### Retrieving account details - -The client can retrieve the current account details for a user using a `GET` -request to `/account/details`. The user has to be logged in. The returned body -will have the content type `application/json` and the following format: - -```json -{ - "email": "optional@email.address" -} -``` - -#### Changing account details - -To change the email address or password for an existing user, the client may -send a `POST` request to `/account/details`. The content type has to be -`application/json` and the body should contain a valid JSON object in the -following form: - -```json -{ - "username": "username", - "password": "old password", - "newEmail": "optional@email.address", - "newPassword": "new password" -} -``` - -The `newEmail` and `newPassword` parameters both can be left out or set to null -to indicate that they remain unchanged. `username` and `password` have to be -provided. If the user doesn't exist or the old password was wrong, an error -`403` will be returned. - -#### Deleting an account - -To delete an existing account, the client may send a `POST` request to -`/account/delete`. The content type has to be `application/json` and the body -should contain a valid JSON object in the following form: - -```json -{ - "username": "username", - "password": "password" -} -``` - -If the user doesn't exist or the password was wrong, an error `403` will be -returned. - -### Adding new entities - -To be able to add new entities, the user has to be authenticated and authorized -to do so. By default, this is the case for newly registered users. The content -type should be `application/json` and the body should contain a valid JSON -object matching the specific resource. The entity ID should be generated on -the client side to facilitate offline usage. This means, that entity creation -will be handled using `PUT` requests to the following routes: - -- `/persons/{id}` -- `/instruments/{id}` -- `/works/{id}` -- `/ensembles/{id}` -- `/recordings/{id}` - -The following errors may occur: - -| Error code | Explanation | -| ---------- | ---------------------------------------- | -| `400` | The body was malformed. | -| `415` | Content type was not `application/json`. | - - -### Editing existing entities - -To be able to edit existing entities, the user has to be authenticated and -authorized to do so. By default, newly registered users are not allowed to edit -entities. The interface is exactly the same as the one for adding new entities. - -### Deleting entities - -To be able to delete existing entities, the user has to be authenticated and -authorized to do so. By default, newly registered users are not allowed to -delete entities. The following routes handle `DELETE` requests for deleting -entities: - -- `/persons/{id}` -- `/instruments/{id}` -- `/works/{id}` -- `/ensembles/{id}` -- `/recordings/{id}` - -If the entity doesn't exist, an error `404` will be returned. \ No newline at end of file diff --git a/server/bin/main.dart b/server/bin/main.dart deleted file mode 100644 index 176c9e6..0000000 --- a/server/bin/main.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:aqueduct/aqueduct.dart'; -import 'package:musicus_server/musicus_server.dart'; - -Future main() async { - final configFilePath = 'config.yaml'; - final config = MusicusServerConfiguration(configFilePath); - - final server = Application() - ..options.configurationFilePath = configFilePath - ..options.address = config.host - ..options.port = config.port; - - await server.start( - consoleLogging: true, - ); - - print('Database: ${config.dbPath ?? 'memory'}'); - print('Server database: ${config.serverDbPath ?? 'memory'}'); - print('Listening on ${config.host}:${config.port}'); -} diff --git a/server/config.src.yaml b/server/config.src.yaml deleted file mode 100644 index 18e5f6b..0000000 --- a/server/config.src.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# A dbPath and serverDbPath of null means that we want in-memory databases. -host: localhost -port: 1833 -secret: vulnerable \ No newline at end of file diff --git a/server/config.yaml b/server/config.yaml deleted file mode 100644 index 5fbf0a6..0000000 --- a/server/config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -host: localhost -port: 1833 -secret: vulnerable -dbPath: db.sqlite -serverDbPath: server.sqlite \ No newline at end of file diff --git a/server/lib/musicus_server.dart b/server/lib/musicus_server.dart deleted file mode 100644 index 148da78..0000000 --- a/server/lib/musicus_server.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'src/configuration.dart'; -export 'src/server.dart'; \ No newline at end of file diff --git a/server/lib/src/auth.dart b/server/lib/src/auth.dart deleted file mode 100644 index 0331686..0000000 --- a/server/lib/src/auth.dart +++ /dev/null @@ -1,293 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:aqueduct/aqueduct.dart'; -import 'package:corsac_jwt/corsac_jwt.dart'; - -import 'compute.dart'; -import 'crypt.dart'; -import 'database.dart'; - -/// Information on the user making the request. -extension AuthorizationInfo on Request { - /// The username of the logged in user. - /// - /// If this is a non null value, the user was authenticated. - String get username => this.attachments['username']; - set username(String value) => this.attachments['username'] = value; - - /// Whether the user may create new resources. - /// - /// This can only be true if the user was authenticated. - bool get mayUpload => this.attachments['mayUpload'] ?? false; - set mayUpload(bool value) => this.attachments['mayUpload'] = value; - - /// Whether the user may edit existing resources. - /// - /// This can only be true if the user was authenticated. - bool get mayEdit => this.attachments['mayEdit'] ?? false; - set mayEdit(bool value) => this.attachments['mayEdit'] = value; - - /// Whether the user may delete resources. - /// - /// This can only be true if the user was authenticated. - bool get mayDelete => this.attachments['mayDelete'] ?? false; - set mayDelete(bool value) => this.attachments['mayDelete'] = value; -} - -/// Endpoint controller for user registration. -/// -/// This expects a POST request with a JSON body representing a [RequestUser]. -class RegisterController extends Controller { - final ServerDatabase db; - - RegisterController(this.db); - - @override - Future handle(Request request) async { - if (request.method == 'POST') { - final json = await request.body.decode>(); - - final String username = json['username']; - final String email = json['email']; - final String password = json['password']; - - // Check if we already have a user with that name. - final existingUser = await db.getUser(username); - if (existingUser != null) { - // Returning something different than 200 here has the security - // implication that an attacker can check for existing user names. At - // the moment, I don't see any alternatives, because we don't use email - // addresses for identification. The client needs to know, whether the - // user name is already given. - return Response.conflict(); - } else { - // This will take a long time, so we run it in a new isolate. - final result = await compute(Crypt.hashPassword, password); - - db.updateUser(User( - name: username, - email: email, - salt: result.salt, - hash: result.hash, - mayUpload: true, - mayEdit: false, - mayDelete: false, - )); - - return Response.ok(null); - } - } else { - return Response(HttpStatus.methodNotAllowed, null, null); - } - } -} - -/// Endpoint controller for user login. -/// -/// This expects a POST request with a JSON body representing a [RequestUser]. -class LoginController extends Controller { - final ServerDatabase db; - - /// The secret that will be used for signing the token. - final String secret; - - final JWTHmacSha256Signer _signer; - - LoginController(this.db, this.secret) : _signer = JWTHmacSha256Signer(secret); - - @override - Future handle(Request request) async { - if (request.method == 'POST') { - final json = await request.body.decode>(); - - final String username = json['username']; - final String password = json['password']; - - final user = await db.getUser(username); - if (user != null) { - // We check the password in a new isolate, because this can take a long - // time. - if (await compute( - Crypt.checkPassword, - CheckPasswordRequest( - password: password, - salt: user.salt, - hash: user.hash, - ), - )) { - final builder = JWTBuilder() - ..expiresAt = DateTime.now().add(Duration(minutes: 30)) - ..setClaim('user', username); - - final token = builder.getSignedToken(_signer).toString(); - - return Response.ok(token, headers: {'Content-Type': 'text/plain'}); - } - } - - return Response.unauthorized(); - } - - return Response(HttpStatus.methodNotAllowed, null, null); - } -} - -/// An endpoint controller for retrieving and changing account details. -class AccountDetailsController extends Controller { - final ServerDatabase db; - - AccountDetailsController(this.db); - - @override - Future handle(Request request) async { - if (request.method == 'GET') { - if (request.username != null) { - final user = await db.getUser(request.username); - return Response.ok({ - 'email': user.email, - }); - } else { - return Response.forbidden(); - } - } else if (request.method == 'POST') { - final json = await request.body.decode>(); - - final String username = json['username']; - final String password = json['password']; - final String newEmail = json['newEmail']; - final String newPassword = json['newPassword']; - - final user = await db.getUser(username); - - // Check whether the user exists and the password was right. - if (user != null && - await compute( - Crypt.checkPassword, - CheckPasswordRequest( - password: password, - salt: user.salt, - hash: user.hash, - ), - )) { - HashPasswordResult hashResult; - - if (newPassword != null) { - hashResult = await compute(Crypt.hashPassword, newPassword); - } else { - hashResult = HashPasswordResult( - hash: user.hash, - salt: user.salt, - ); - } - - db.updateUser(User( - name: username, - email: newEmail ?? user.email, - salt: hashResult.salt, - hash: hashResult.hash, - mayUpload: user.mayUpload, - mayEdit: user.mayEdit, - mayDelete: user.mayDelete, - )); - - return Response.ok(null); - } else { - return Response.forbidden(); - } - } else { - return Response(HttpStatus.methodNotAllowed, null, null); - } - } -} - -/// An endpoint controller for deleting an account. -class AccountDeleteController extends Controller { - final ServerDatabase db; - - AccountDeleteController(this.db); - - @override - Future handle(Request request) async { - if (request.method == 'POST') { - final json = await request.body.decode>(); - - final String username = json['username']; - final String password = json['password']; - - final user = await db.getUser(username); - - // Check whether the user exists and the password was right. - if (user != null && - await compute( - Crypt.checkPassword, - CheckPasswordRequest( - password: password, - salt: user.salt, - hash: user.hash, - ), - )) { - await db.deleteUser(username); - - return Response.ok(null); - } else { - return Response.forbidden(); - } - } else { - return Response(HttpStatus.methodNotAllowed, null, null); - } - } -} - -/// Middleware for checking authorization. -/// -/// This will set the fields defined in [AuthorizationInfo] on this request -/// according to the provided access token. -class AuthorizationController extends Controller { - final ServerDatabase db; - - /// The secret that was used to sign the token. - final String secret; - - final JWTHmacSha256Signer _signer; - - AuthorizationController(this.db, this.secret) - : _signer = JWTHmacSha256Signer(secret); - - @override - FutureOr handle(Request request) async { - final authHeaderValue = - request.raw.headers.value(HttpHeaders.authorizationHeader); - - if (authHeaderValue != null) { - final authHeaderParts = authHeaderValue.split(' '); - - if (authHeaderParts.length == 2 && authHeaderParts[0] == 'Bearer') { - final jwt = JWT.parse(authHeaderParts[1]); - - /// The JWTValidator will automatically use the current time. An empty - /// result will mean that the token is valid and its signature was - /// verified. - if (JWTValidator().validate(jwt, signer: _signer).isEmpty) { - final user = await db.getUser(jwt.claims['user']); - if (user != null) { - request.username = user.name; - request.mayUpload = user.mayUpload; - request.mayEdit = user.mayEdit; - request.mayDelete = user.mayDelete; - - return request; - } else { - return Response.unauthorized(); - } - } else { - return Response.unauthorized(); - } - } else { - return Response.badRequest(); - } - } else { - return request; - } - } -} diff --git a/server/lib/src/compositions.dart b/server/lib/src/compositions.dart deleted file mode 100644 index d67eb7f..0000000 --- a/server/lib/src/compositions.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:aqueduct/aqueduct.dart'; -import 'package:musicus_database/musicus_database.dart'; - -class CompositionsController extends ResourceController { - final Database db; - - CompositionsController(this.db); - - @Operation.get('id') - Future getWorks(@Bind.path('id') int id, - {@Bind.query('p') int page, @Bind.query('s') String search}) async { - final works = await db.getWorks(id, page, search); - return Response.ok(works); - } -} diff --git a/server/lib/src/compute.dart b/server/lib/src/compute.dart deleted file mode 100644 index c29c5ef..0000000 --- a/server/lib/src/compute.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'dart:isolate'; - -import 'package:meta/meta.dart'; - -/// This function will run within the new isolate. -void _isolateEntrypoint(_ComputeRequest request) { - final result = request.compute(); - request.sendPort.send(result); -} - -/// Bundle of information to pass to the isolate. -class _ComputeRequest { - /// The function to call. - T Function(S parameter) function; - - /// The parameter to pass to the function. - S parameter; - - /// The port through which the result will be sent. - SendPort sendPort; - - _ComputeRequest({ - @required this.function, - @required this.parameter, - @required this.sendPort, - }); - - /// Call [function] with [parameter] and return the result. - /// - /// This function exists to avoid type errors within the isolate. - T compute() => function(parameter); -} - -/// Call a function in a new isolate and await the result. -/// -/// The function has to be a static function. If the result is not a primitive -/// value or a list or map of such, this won't work -/// (see https://api.dart.dev/stable/2.8.1/dart-isolate/SendPort/send.html). -Future compute(T Function(S parameter) function, S parameter) async { - final receivePort = ReceivePort(); - - Isolate.spawn( - _isolateEntrypoint, - _ComputeRequest( - function: function, - parameter: parameter, - sendPort: receivePort.sendPort, - ), - ); - - return await receivePort.first as T; -} diff --git a/server/lib/src/configuration.dart b/server/lib/src/configuration.dart deleted file mode 100644 index 9cd842a..0000000 --- a/server/lib/src/configuration.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:io'; - -import 'package:aqueduct/aqueduct.dart'; - -class MusicusServerConfiguration extends Configuration { - MusicusServerConfiguration(String fileName) : super.fromFile(File(fileName)); - - String host; - int port; - String secret; - - @optionalConfiguration - String dbPath; - - @optionalConfiguration - String serverDbPath; -} \ No newline at end of file diff --git a/server/lib/src/crypt.dart b/server/lib/src/crypt.dart deleted file mode 100644 index e33bf5b..0000000 --- a/server/lib/src/crypt.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; - -import 'package:meta/meta.dart'; -import 'package:steel_crypt/steel_crypt.dart'; - -/// Result of [hashPassword]. -class HashPasswordResult { - /// The computed hash. - final String hash; - - /// A randomly generated string. - final String salt; - - HashPasswordResult({ - @required this.hash, - @required this.salt, - }); -} - -/// Parameters for [checkPassword]. -class CheckPasswordRequest { - /// The password to check. - final String password; - - /// The salt that was used for computing the hash. - final String salt; - - /// The hash value to check against. - final String hash; - - CheckPasswordRequest({ - @required this.password, - @required this.salt, - @required this.hash, - }); -} - -/// Methods for handling passwords. -class Crypt { - static final _crypt = PassCrypt.pbkdf2(hmac: HmacHash.Sha_512); - static final _rand = Random.secure(); - - /// Compute a hash for a password. - /// - /// The result will contain the hash and a randomly generated salt. - static HashPasswordResult hashPassword(String password) { - final bytes = List.generate(32, (i) => _rand.nextInt(256)); - final salt = base64UrlEncode(bytes); - final hash = _crypt.hashPass(salt, password); - - return HashPasswordResult( - hash: hash, - salt: salt, - ); - } - - /// Check whether a password matches a hash value. - static bool checkPassword(CheckPasswordRequest request) { - return _crypt.checkPassKey(request.salt, request.password, request.hash); - } -} diff --git a/server/lib/src/database.dart b/server/lib/src/database.dart deleted file mode 100644 index 88b08dd..0000000 --- a/server/lib/src/database.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:moor/moor.dart'; - -part 'database.g.dart'; - -@UseMoor(include: {'database.moor'}) -class ServerDatabase extends _$ServerDatabase { - @override - final schemaVersion = 0; - - ServerDatabase(QueryExecutor e) : super(e); - - Future getUser(String name) async { - return await (select(users)..where((u) => u.name.equals(name))).getSingle(); - } - - Future updateUser(User user) async { - await into(users).insert(user, mode: InsertMode.insertOrReplace); - } - - Future deleteUser(String name) async { - await (delete(users)..where((u) => u.name.equals(name))).go(); - } -} diff --git a/server/lib/src/database.moor b/server/lib/src/database.moor deleted file mode 100644 index 0c556df..0000000 --- a/server/lib/src/database.moor +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE users ( - name TEXT PRIMARY KEY, - email TEXT, - salt TEXT NOT NULL, - hash TEXT NOT NULL, - may_upload BOOLEAN NOT NULL DEFAULT TRUE, - may_edit BOOLEAN NOT NULL DEFAULT FALSE, - may_delete BOOLEAN NOT NULL DEFAULT FALSE -); \ No newline at end of file diff --git a/server/lib/src/ensembles.dart b/server/lib/src/ensembles.dart deleted file mode 100644 index 46babb1..0000000 --- a/server/lib/src/ensembles.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:aqueduct/aqueduct.dart'; -import 'package:musicus_database/musicus_database.dart'; - -import 'auth.dart'; - -class EnsemblesController extends ResourceController { - final Database db; - - EnsemblesController(this.db); - - @Operation.get() - Future getEnsembles( - {@Bind.query('p') int page, @Bind.query('s') String search}) async { - final ensembles = await db.getEnsembles(page, search); - return Response.ok(ensembles); - } - - @Operation.get('id') - Future getEnsemble(@Bind.path('id') int id) async { - final ensemble = await db.ensembleById(id).getSingle(); - if (ensemble != null) { - return Response.ok(ensemble); - } else { - return Response.notFound(); - } - } - - @Operation.put('id') - Future putEnsemble( - @Bind.path('id') int id, @Bind.body() Map json) async { - if (await db.ensembleById(id).getSingle() != null) { - if (!request.mayEdit) { - return Response.forbidden(); - } - } else { - if (!request.mayUpload) { - return Response.forbidden(); - } - } - - final ensemble = Ensemble.fromJson(json).copyWith( - id: id, - ); - - await db.updateEnsemble(ensemble); - - return Response.ok(null); - } - - @Operation.delete('id') - Future deleteEnsemble(@Bind.path('id') int id) async { - if (!request.mayDelete) { - return Response.forbidden(); - } - - await db.deleteEnsemble(id); - return Response.ok(null); - } -} diff --git a/server/lib/src/instruments.dart b/server/lib/src/instruments.dart deleted file mode 100644 index f93d158..0000000 --- a/server/lib/src/instruments.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:aqueduct/aqueduct.dart'; -import 'package:musicus_database/musicus_database.dart'; - -import 'auth.dart'; - -class InstrumentsController extends ResourceController { - final Database db; - - InstrumentsController(this.db); - - @Operation.get() - Future getInstruments( - {@Bind.query('p') int page, @Bind.query('s') String search}) async { - final instruments = await db.getInstruments(page, search); - return Response.ok(instruments); - } - - @Operation.get('id') - Future getInstrument(@Bind.path('id') int id) async { - final instrument = await db.instrumentById(id).getSingle(); - if (instrument != null) { - return Response.ok(instrument); - } else { - return Response.notFound(); - } - } - - @Operation.put('id') - Future putInstrument( - @Bind.path('id') int id, @Bind.body() Map json) async { - if (await db.instrumentById(id).getSingle() != null) { - if (!request.mayEdit) { - return Response.forbidden(); - } - } else { - if (!request.mayUpload) { - return Response.forbidden(); - } - } - - final instrument = Instrument.fromJson(json).copyWith( - id: id, - ); - - await db.updateInstrument(instrument); - - return Response.ok(null); - } - - @Operation.delete('id') - Future deleteInstrument(@Bind.path('id') int id) async { - if (!request.mayDelete) { - return Response.forbidden(); - } - - await db.deleteInstrument(id); - return Response.ok(null); - } -} diff --git a/server/lib/src/persons.dart b/server/lib/src/persons.dart deleted file mode 100644 index 9e3542f..0000000 --- a/server/lib/src/persons.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:aqueduct/aqueduct.dart'; -import 'package:musicus_database/musicus_database.dart'; - -import 'auth.dart'; - -class PersonsController extends ResourceController { - final Database db; - - PersonsController(this.db); - - @Operation.get() - Future getPersons( - {@Bind.query('p') int page, @Bind.query('s') String search}) async { - final persons = await db.getPersons(page, search); - return Response.ok(persons); - } - - @Operation.get('id') - Future getPerson(@Bind.path('id') int id) async { - final person = await db.personById(id).getSingle(); - if (person != null) { - return Response.ok(person); - } else { - return Response.notFound(); - } - } - - @Operation.put('id') - Future putPerson( - @Bind.path('id') int id, @Bind.body() Map json) async { - if (await db.personById(id).getSingle() != null) { - if (!request.mayEdit) { - return Response.forbidden(); - } - } else { - if (!request.mayUpload) { - return Response.forbidden(); - } - } - - final person = Person.fromJson(json).copyWith( - id: id, - ); - - await db.updatePerson(person); - - return Response.ok(null); - } - - @Operation.delete('id') - Future deletePerson(@Bind.path('id') int id) async { - if (!request.mayDelete) { - return Response.forbidden(); - } - - await db.deletePerson(id); - return Response.ok(null); - } -} diff --git a/server/lib/src/recordings.dart b/server/lib/src/recordings.dart deleted file mode 100644 index 57bd202..0000000 --- a/server/lib/src/recordings.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:aqueduct/aqueduct.dart'; -import 'package:musicus_database/musicus_database.dart'; - -import 'auth.dart'; - -class RecordingsController extends ResourceController { - final Database db; - - RecordingsController(this.db); - - @Operation.get('id') - Future getRecording(@Bind.path('id') int id) async { - final recording = await db.getRecording(id); - if (recording != null) { - return Response.ok(recording); - } else { - return Response.notFound(); - } - } - - @Operation.put('id') - Future putRecording( - @Bind.path('id') int id, @Bind.body() Map json) async { - if (await db.recordingById(id).getSingle() != null) { - if (!request.mayEdit) { - return Response.forbidden(); - } - } else { - if (!request.mayUpload) { - return Response.forbidden(); - } - } - - final recordingInfo = RecordingInfo.fromJson(json); - await db.updateRecording(recordingInfo); - - return Response.ok(null); - } - - @Operation.delete('id') - Future deleteRecording(@Bind.path('id') int id) async { - if (!request.mayDelete) { - return Response.forbidden(); - } - - await db.deleteRecording(id); - return Response.ok(null); - } -} diff --git a/server/lib/src/server.dart b/server/lib/src/server.dart deleted file mode 100644 index 68acc39..0000000 --- a/server/lib/src/server.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:io'; - -import 'package:aqueduct/aqueduct.dart'; -import 'package:moor_ffi/moor_ffi.dart'; -import 'package:musicus_database/musicus_database.dart'; - -import 'auth.dart'; -import 'compositions.dart'; -import 'configuration.dart'; -import 'database.dart'; -import 'ensembles.dart'; -import 'instruments.dart'; -import 'persons.dart'; -import 'recordings.dart'; -import 'works.dart'; -import 'work_recordings.dart'; - -class MusicusServer extends ApplicationChannel { - Database db; - ServerDatabase serverDb; - String secret; - - @override - Future prepare() async { - final config = MusicusServerConfiguration(options.configurationFilePath); - - if (config.dbPath != null) { - db = Database(VmDatabase(File(config.dbPath))); - } else { - db = Database(VmDatabase.memory()); - } - - if (config.serverDbPath != null) { - serverDb = ServerDatabase(VmDatabase(File(config.serverDbPath))); - } else { - serverDb = ServerDatabase(VmDatabase.memory()); - } - - secret = config.secret; - } - - @override - Controller get entryPoint => Router() - ..route('/account/register').link(() => RegisterController(serverDb)) - ..route('/account/details') - .link(() => AuthorizationController(serverDb, secret)) - .link(() => AccountDetailsController(serverDb)) - ..route('/account/delete').link(() => AccountDeleteController(serverDb)) - ..route('/account/login').link(() => LoginController(serverDb, secret)) - ..route('/persons/[:id]') - .link(() => AuthorizationController(serverDb, secret)) - .link(() => PersonsController(db)) - ..route('/persons/:id/works').link(() => CompositionsController(db)) - ..route('/instruments/[:id]') - .link(() => AuthorizationController(serverDb, secret)) - .link(() => InstrumentsController(db)) - ..route('/works/:id') - .link(() => AuthorizationController(serverDb, secret)) - .link(() => WorksController(db)) - ..route('/works/:id/recordings').link(() => WorkRecordingsController(db)) - ..route('/ensembles/[:id]') - .link(() => AuthorizationController(serverDb, secret)) - .link(() => EnsemblesController(db)) - ..route('/recordings/:id') - .link(() => AuthorizationController(serverDb, secret)) - .link(() => RecordingsController(db)); -} diff --git a/server/lib/src/work_recordings.dart b/server/lib/src/work_recordings.dart deleted file mode 100644 index 80eb741..0000000 --- a/server/lib/src/work_recordings.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:aqueduct/aqueduct.dart'; -import 'package:musicus_database/musicus_database.dart'; - -class WorkRecordingsController extends ResourceController { - final Database db; - - WorkRecordingsController(this.db); - - @Operation.get('id') - Future getRecordings(@Bind.path('id') int id, - {@Bind.query('p') int page}) async { - final recordings = await db.getRecordings(id, page); - return Response.ok(recordings); - } -} diff --git a/server/lib/src/works.dart b/server/lib/src/works.dart deleted file mode 100644 index b0777c7..0000000 --- a/server/lib/src/works.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:aqueduct/aqueduct.dart'; -import 'package:musicus_database/musicus_database.dart'; - -import 'auth.dart'; - -class WorksController extends ResourceController { - final Database db; - - WorksController(this.db); - - @Operation.get('id') - Future getWork(@Bind.path('id') int id) async { - final work = await db.getWork(id); - if (work != null) { - return Response.ok(work); - } else { - return Response.notFound(); - } - } - - @Operation.put('id') - Future putWork( - @Bind.path('id') int id, @Bind.body() Map json) async { - if (await db.workById(id).getSingle() != null) { - if (!request.mayEdit) { - return Response.forbidden(); - } - } else { - if (!request.mayUpload) { - return Response.forbidden(); - } - } - - final workInfo = WorkInfo.fromJson(json); - await db.updateWork(workInfo); - - return Response.ok(null); - } - - @Operation.delete('id') - Future deleteWork(@Bind.path('id') int id) async { - if (!request.mayDelete) { - return Response.forbidden(); - } - - await db.deleteWork(id); - return Response.ok(null); - } -} diff --git a/server/pubspec.yaml b/server/pubspec.yaml deleted file mode 100644 index 5f71639..0000000 --- a/server/pubspec.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: musicus_server -description: A server hosting a Musicus database. -version: 0.0.1 - -environment: - sdk: ">=2.6.0 <3.0.0" - -dependencies: - aqueduct: - corsac_jwt: - meta: - moor: ^3.0.2 - moor_ffi: ^0.5.0 - musicus_database: - path: ../database - steel_crypt: ^2.0.3 - -dev_dependencies: - build_runner: - moor_generator: ^3.0.0