mirror of
https://github.com/johrpan/musicus_mobile.git
synced 2025-10-25 19:27:24 +02:00
Remove server package
This commit is contained in:
parent
608726f555
commit
b36fe340ad
29 changed files with 0 additions and 1807 deletions
|
|
@ -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
|
||||
|
|
|
|||
31
database/.gitignore
vendored
31
database/.gitignore
vendored
|
|
@ -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/
|
||||
|
|
@ -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
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export 'src/database.dart';
|
||||
export 'src/info.dart';
|
||||
|
|
@ -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<List<Person>> getPersons([int page, String search]) async {
|
||||
final offset = page != null ? page * pageSize : 0;
|
||||
List<Person> 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<void> updatePerson(Person person) async {
|
||||
await into(persons).insert(
|
||||
person,
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
/// Delete the person by [id].
|
||||
Future<void> 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<List<Instrument>> getInstruments([int page, String search]) async {
|
||||
final offset = page != null ? page * pageSize : 0;
|
||||
List<Instrument> 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<void> updateInstrument(Instrument instrument) async {
|
||||
await into(instruments).insert(
|
||||
instrument,
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
/// Delete the instrument by [id].
|
||||
Future<void> deleteInstrument(int id) async {
|
||||
await (delete(instruments)..where((i) => i.id.equals(id))).go();
|
||||
}
|
||||
|
||||
/// Retrieve more information on an already queried work.
|
||||
Future<WorkInfo> 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<PartInfo> 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<WorkSection> 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<WorkInfo> 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<List<WorkInfo>> getWorks(int personId,
|
||||
[int page, String search]) async {
|
||||
final offset = page != null ? page * pageSize : 0;
|
||||
List<Work> works;
|
||||
|
||||
if (search == null || search.isEmpty) {
|
||||
works = await worksByComposer(personId, pageSize, offset).get();
|
||||
} else {
|
||||
works =
|
||||
await searchWorksByComposer(personId, '$search%', pageSize, offset)
|
||||
.get();
|
||||
}
|
||||
|
||||
final List<WorkInfo> 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<void> 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<void> 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<List<Ensemble>> getEnsembles([int page, String search]) async {
|
||||
final offset = page != null ? page * pageSize : 0;
|
||||
List<Ensemble> 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<void> updateEnsemble(Ensemble ensemble) async {
|
||||
await into(ensembles).insert(
|
||||
ensemble,
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
/// Delete the ensemble by [id].
|
||||
Future<void> 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<void> 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<RecordingInfo> getRecordingInfo(Recording recording) async {
|
||||
final id = recording.id;
|
||||
|
||||
final List<PerformanceInfo> 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<RecordingInfo> getRecording(int id) async {
|
||||
final recording = await recordingById(id).getSingle();
|
||||
return await getRecordingInfo(recording);
|
||||
}
|
||||
|
||||
/// Delete a recording by [id].
|
||||
Future<void> 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<List<RecordingInfo>> getRecordings(int workId, [int page]) async {
|
||||
final offset = page != null ? page * pageSize : 0;
|
||||
final recordings = await recordingsByWork(workId, pageSize, offset).get();
|
||||
|
||||
final List<RecordingInfo> result = [];
|
||||
for (final recording in recordings) {
|
||||
result.add(await getRecordingInfo(recording));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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<Instrument> 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<String, dynamic> json) => PartInfo(
|
||||
part: WorkPart.fromJson(json['part']),
|
||||
instruments: json['instruments']
|
||||
.map<Instrument>((j) => Instrument.fromJson(j))
|
||||
.toList(),
|
||||
composer:
|
||||
json['composer'] != null ? Person.fromJson(json['composer']) : null,
|
||||
);
|
||||
|
||||
Map<String, dynamic> 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<Instrument> instruments;
|
||||
|
||||
/// A list of persons, which will include all part composers.
|
||||
final List<Person> composers;
|
||||
|
||||
/// All available information on the work parts.
|
||||
final List<PartInfo> parts;
|
||||
|
||||
/// The sections of this work.
|
||||
final List<WorkSection> sections;
|
||||
|
||||
WorkInfo({
|
||||
this.work,
|
||||
this.instruments,
|
||||
this.composers,
|
||||
this.parts,
|
||||
this.sections,
|
||||
});
|
||||
|
||||
factory WorkInfo.fromJson(Map<String, dynamic> json) => WorkInfo(
|
||||
work: Work.fromJson(json['work']),
|
||||
instruments: json['instruments']
|
||||
.map<Instrument>((j) => Instrument.fromJson(j))
|
||||
.toList(),
|
||||
composers:
|
||||
json['composers'].map<Person>((j) => Person.fromJson(j)).toList(),
|
||||
parts:
|
||||
json['parts'].map<PartInfo>((j) => PartInfo.fromJson(j)).toList(),
|
||||
sections: json['sections']
|
||||
.map<WorkSection>((j) => WorkSection.fromJson(j))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<PerformanceInfo> performances;
|
||||
|
||||
RecordingInfo({
|
||||
this.recording,
|
||||
this.performances,
|
||||
});
|
||||
|
||||
factory RecordingInfo.fromJson(Map<String, dynamic> json) => RecordingInfo(
|
||||
recording: Recording.fromJson(json['recording']),
|
||||
performances: json['performances']
|
||||
.map<PerformanceInfo>((j) => PerformanceInfo.fromJson(j))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'recording': recording.toJson(),
|
||||
'performances': performances.map((p) => p.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
|
@ -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:
|
||||
31
server/.gitignore
vendored
31
server/.gitignore
vendored
|
|
@ -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/
|
||||
207
server/README.md
207
server/README.md
|
|
@ -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.
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import 'package:aqueduct/aqueduct.dart';
|
||||
import 'package:musicus_server/musicus_server.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
final configFilePath = 'config.yaml';
|
||||
final config = MusicusServerConfiguration(configFilePath);
|
||||
|
||||
final server = Application<MusicusServer>()
|
||||
..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}');
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# A dbPath and serverDbPath of null means that we want in-memory databases.
|
||||
host: localhost
|
||||
port: 1833
|
||||
secret: vulnerable
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
host: localhost
|
||||
port: 1833
|
||||
secret: vulnerable
|
||||
dbPath: db.sqlite
|
||||
serverDbPath: server.sqlite
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export 'src/configuration.dart';
|
||||
export 'src/server.dart';
|
||||
|
|
@ -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<Response> handle(Request request) async {
|
||||
if (request.method == 'POST') {
|
||||
final json = await request.body.decode<Map<String, dynamic>>();
|
||||
|
||||
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<Response> handle(Request request) async {
|
||||
if (request.method == 'POST') {
|
||||
final json = await request.body.decode<Map<String, dynamic>>();
|
||||
|
||||
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<Response> 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<Map<String, dynamic>>();
|
||||
|
||||
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<Response> handle(Request request) async {
|
||||
if (request.method == 'POST') {
|
||||
final json = await request.body.decode<Map<String, dynamic>>();
|
||||
|
||||
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<RequestOrResponse> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Response> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import 'dart:isolate';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// This function will run within the new isolate.
|
||||
void _isolateEntrypoint<T, S>(_ComputeRequest<T, S> request) {
|
||||
final result = request.compute();
|
||||
request.sendPort.send(result);
|
||||
}
|
||||
|
||||
/// Bundle of information to pass to the isolate.
|
||||
class _ComputeRequest<T, S> {
|
||||
/// 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<T> compute<T, S>(T Function(S parameter) function, S parameter) async {
|
||||
final receivePort = ReceivePort();
|
||||
|
||||
Isolate.spawn(
|
||||
_isolateEntrypoint,
|
||||
_ComputeRequest<T, S>(
|
||||
function: function,
|
||||
parameter: parameter,
|
||||
sendPort: receivePort.sendPort,
|
||||
),
|
||||
);
|
||||
|
||||
return await receivePort.first as T;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<User> getUser(String name) async {
|
||||
return await (select(users)..where((u) => u.name.equals(name))).getSingle();
|
||||
}
|
||||
|
||||
Future<void> updateUser(User user) async {
|
||||
await into(users).insert(user, mode: InsertMode.insertOrReplace);
|
||||
}
|
||||
|
||||
Future<void> deleteUser(String name) async {
|
||||
await (delete(users)..where((u) => u.name.equals(name))).go();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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<Response> 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<Response> 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<Response> putEnsemble(
|
||||
@Bind.path('id') int id, @Bind.body() Map<String, dynamic> 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<Response> deleteEnsemble(@Bind.path('id') int id) async {
|
||||
if (!request.mayDelete) {
|
||||
return Response.forbidden();
|
||||
}
|
||||
|
||||
await db.deleteEnsemble(id);
|
||||
return Response.ok(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<Response> 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<Response> 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<Response> putInstrument(
|
||||
@Bind.path('id') int id, @Bind.body() Map<String, dynamic> 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<Response> deleteInstrument(@Bind.path('id') int id) async {
|
||||
if (!request.mayDelete) {
|
||||
return Response.forbidden();
|
||||
}
|
||||
|
||||
await db.deleteInstrument(id);
|
||||
return Response.ok(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<Response> 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<Response> 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<Response> putPerson(
|
||||
@Bind.path('id') int id, @Bind.body() Map<String, dynamic> 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<Response> deletePerson(@Bind.path('id') int id) async {
|
||||
if (!request.mayDelete) {
|
||||
return Response.forbidden();
|
||||
}
|
||||
|
||||
await db.deletePerson(id);
|
||||
return Response.ok(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<Response> 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<Response> putRecording(
|
||||
@Bind.path('id') int id, @Bind.body() Map<String, dynamic> 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<Response> deleteRecording(@Bind.path('id') int id) async {
|
||||
if (!request.mayDelete) {
|
||||
return Response.forbidden();
|
||||
}
|
||||
|
||||
await db.deleteRecording(id);
|
||||
return Response.ok(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<void> 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));
|
||||
}
|
||||
|
|
@ -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<Response> getRecordings(@Bind.path('id') int id,
|
||||
{@Bind.query('p') int page}) async {
|
||||
final recordings = await db.getRecordings(id, page);
|
||||
return Response.ok(recordings);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Response> 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<Response> putWork(
|
||||
@Bind.path('id') int id, @Bind.body() Map<String, dynamic> 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<Response> deleteWork(@Bind.path('id') int id) async {
|
||||
if (!request.mayDelete) {
|
||||
return Response.forbidden();
|
||||
}
|
||||
|
||||
await db.deleteWork(id);
|
||||
return Response.ok(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
|
||||
Loading…
Add table
Add a link
Reference in a new issue