client: Implement authorization

This commit is contained in:
Elias Projahn 2020-05-08 19:02:39 +02:00
parent 9e0c6fa00a
commit fa2e9ebacd

View file

@ -5,6 +5,30 @@ import 'package:http/http.dart' as http;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:musicus_database/musicus_database.dart'; import 'package:musicus_database/musicus_database.dart';
/// A user of the Musicus API.
class User {
/// Username.
final String name;
/// An optional email address.
final String email;
/// The user's password.
final String password;
User({
this.name,
this.email,
this.password,
});
Map<String, dynamic> toJson() => {
'name': name,
'email': email,
'password': password,
};
}
/// A simple http client for the Musicus server. /// A simple http client for the Musicus server.
class MusicusClient { class MusicusClient {
/// URI scheme to use for the connection. /// URI scheme to use for the connection.
@ -23,16 +47,30 @@ class MusicusClient {
/// Base path to the root location of the Musicus API. /// Base path to the root location of the Musicus API.
final String basePath; final String basePath;
User _user;
/// The user to login.
User get user => _user;
set user(User user) {
_user = user;
_token = null;
}
final _client = http.Client(); final _client = http.Client();
/// The last retrieved access token.
String _token;
MusicusClient({ MusicusClient({
this.scheme = 'https', this.scheme = 'https',
@required this.host, @required this.host,
this.port = 443, this.port = 443,
this.basePath, this.basePath,
User user,
}) : assert(scheme != null), }) : assert(scheme != null),
assert(port != null), assert(port != null),
assert(host != null); assert(host != null),
_user = user;
/// Create an URI using member variables and parameters. /// Create an URI using member variables and parameters.
Uri createUri({ Uri createUri({
@ -48,6 +86,91 @@ class MusicusClient {
); );
} }
/// Register a new user.
///
/// This will return true, if the action was successful. Subsequent requests
/// will automatically be made as the new user.
Future<bool> register(User newUser) async {
final response = await _client.post(
createUri(
path: '/register',
),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(newUser.toJson()),
);
if (response.statusCode == HttpStatus.ok) {
_user = newUser;
_token = null;
return true;
} else {
return false;
}
}
/// Retrieve an access token for [user].
///
/// The token will land in [_token]. If the login failed, a
/// [MusicusLoginFailedException] will be thrown.
Future<void> _login() async {
final response = await _client.post(
createUri(
path: '/login',
),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(user.toJson()),
);
if (response.statusCode == HttpStatus.ok) {
_token = response.body;
} else {
throw MusicusLoginFailedException();
}
}
/// Make a request with authorization.
///
/// This will ensure, that the request will be made with a valid
/// authorization header. If [user] is null, this will throw a
/// [MusicusNotLoggedInException]. If it is neccessary, this will login the
/// user and throw a [MusicusLoginFailedException] if that failed. If the
/// user is not authorized to perform the requested action, this will throw
/// a [MusicusNotAuthorizedException].
Future<http.Response> _authorized(String method, Uri uri,
{Map<String, String> headers, String body}) async {
if (_user != null) {
Future<http.Response> _request() async {
final request = http.Request(method, uri);
request.headers.addAll(headers);
request.headers['Authorization'] = 'Bearer $_token';
request.body = body;
return await http.Response.fromStream(await _client.send(request));
}
http.Response response;
if (_token != null) {
response = await _request();
if (response.statusCode == HttpStatus.unauthorized) {
await _login();
response = await _request();
}
} else {
await _login();
response = await _request();
}
if (response.statusCode == HttpStatus.forbidden) {
throw MusicusNotAuthorizedException();
} else {
return response;
}
} else {
throw MusicusNotLoggedInException();
}
}
/// Get a list of persons. /// Get a list of persons.
/// ///
/// You can get another page using the [page] parameter. If a non empty /// You can get another page using the [page] parameter. If a non empty
@ -85,28 +208,28 @@ class MusicusClient {
/// Delete a person by ID. /// Delete a person by ID.
Future<void> deletePerson(int id) async { Future<void> deletePerson(int id) async {
await _client.delete(createUri( await _authorized(
path: '/persons/$id', 'DELETE',
)); createUri(
path: '/persons/$id',
),
);
} }
/// Create or update a person. /// Create or update a person.
/// ///
/// Returns true, if the operation was successful. /// Returns true, if the operation was successful.
Future<bool> putPerson(Person person) async { Future<bool> putPerson(Person person) async {
try { final response = await _authorized(
final response = await _client.put( 'PUT',
createUri( createUri(
path: '/persons/${person.id}', path: '/persons/${person.id}',
), ),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode(person.toJson()), body: jsonEncode(person.toJson()),
); );
return response.statusCode == HttpStatus.ok; return response.statusCode == HttpStatus.ok;
} on Exception {
return false;
}
} }
/// Get a list of instruments. /// Get a list of instruments.
@ -148,26 +271,26 @@ class MusicusClient {
/// ///
/// Returns true, if the operation was successful. /// Returns true, if the operation was successful.
Future<bool> putInstrument(Instrument instrument) async { Future<bool> putInstrument(Instrument instrument) async {
try { final response = await _authorized(
final response = await _client.put( 'PUT',
createUri( createUri(
path: '/instruments/${instrument.id}', path: '/instruments/${instrument.id}',
), ),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode(instrument.toJson()), body: jsonEncode(instrument.toJson()),
); );
return response.statusCode == HttpStatus.ok; return response.statusCode == HttpStatus.ok;
} on Exception {
return false;
}
} }
/// Delete an instrument by ID. /// Delete an instrument by ID.
Future<void> deleteInstrument(int id) async { Future<void> deleteInstrument(int id) async {
await _client.delete(createUri( await _authorized(
path: '/instruments/$id', 'DELETE',
)); createUri(
path: '/instruments/$id',
),
);
} }
/// Get a list of works written by the person with the ID [personId]. /// Get a list of works written by the person with the ID [personId].
@ -208,9 +331,12 @@ class MusicusClient {
/// Delete a work by ID. /// Delete a work by ID.
Future<void> deleteWork(int id) async { Future<void> deleteWork(int id) async {
await _client.delete(createUri( await _authorized(
path: '/works/$id', 'DELETE',
)); createUri(
path: '/works/$id',
),
);
} }
/// Get a list of recordings of the work with the ID [workId]. /// Get a list of recordings of the work with the ID [workId].
@ -236,19 +362,16 @@ class MusicusClient {
/// ///
/// Returns true, if the operation was successful. /// Returns true, if the operation was successful.
Future<bool> putWork(WorkInfo workInfo) async { Future<bool> putWork(WorkInfo workInfo) async {
try { final response = await _authorized(
final response = await _client.put( 'PUT',
createUri( createUri(
path: '/works/${workInfo.work.id}', path: '/works/${workInfo.work.id}',
), ),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode(workInfo.toJson()), body: jsonEncode(workInfo.toJson()),
); );
return response.statusCode == HttpStatus.ok; return response.statusCode == HttpStatus.ok;
} on Exception {
return false;
}
} }
/// Get a list of ensembles. /// Get a list of ensembles.
@ -290,26 +413,26 @@ class MusicusClient {
/// ///
/// Returns true, if the operation was successful. /// Returns true, if the operation was successful.
Future<bool> putEnsemble(Ensemble ensemble) async { Future<bool> putEnsemble(Ensemble ensemble) async {
try { final response = await _authorized(
final response = await _client.put( 'PUT',
createUri( createUri(
path: '/ensembles/${ensemble.id}', path: '/ensembles/${ensemble.id}',
), ),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode(ensemble.toJson()), body: jsonEncode(ensemble.toJson()),
); );
return response.statusCode == HttpStatus.ok; return response.statusCode == HttpStatus.ok;
} on Exception {
return false;
}
} }
/// Delete an ensemble by ID. /// Delete an ensemble by ID.
Future<void> deleteEnsemble(int id) async { Future<void> deleteEnsemble(int id) async {
await _client.delete(createUri( await _authorized(
path: '/ensembles/$id', 'DELETE',
)); createUri(
path: '/ensembles/$id',
),
);
} }
/// Get a recording by ID. /// Get a recording by ID.
@ -326,26 +449,26 @@ class MusicusClient {
/// ///
/// Returns true, if the operation was successful. /// Returns true, if the operation was successful.
Future<bool> putRecording(RecordingInfo recordingInfo) async { Future<bool> putRecording(RecordingInfo recordingInfo) async {
try { final response = await _authorized(
final response = await _client.put( 'PUT',
createUri( createUri(
path: '/recordings/${recordingInfo.recording.id}', path: '/recordings/${recordingInfo.recording.id}',
), ),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode(recordingInfo.toJson()), body: jsonEncode(recordingInfo.toJson()),
); );
return response.statusCode == HttpStatus.ok; return response.statusCode == HttpStatus.ok;
} on Exception {
return false;
}
} }
/// Delete a recording by ID. /// Delete a recording by ID.
Future<void> deleteRecording(int id) async { Future<void> deleteRecording(int id) async {
await _client.delete(createUri( await _authorized(
path: '/recordings/$id', 'DELETE',
)); createUri(
path: '/recordings/$id',
),
);
} }
/// Close the internal http client. /// Close the internal http client.
@ -353,3 +476,26 @@ class MusicusClient {
_client.close(); _client.close();
} }
} }
class MusicusLoginFailedException implements Exception {
MusicusLoginFailedException();
String toString() => 'MusicusLoginFailedException: The username or password '
'was wrong.';
}
class MusicusNotLoggedInException implements Exception {
MusicusNotLoggedInException();
String toString() =>
'MusicusNotLoggedInException: The user must be logged in to perform '
'this action.';
}
class MusicusNotAuthorizedException implements Exception {
MusicusNotAuthorizedException();
String toString() =>
'MusicusNotAuthorizedException: The logged in user is not allowed to '
'perform this action.';
}