handle method
Handles a request.
Implementation
Future<FreedomeResponse> handle(FreedomeRequest r) async {
requests.add(r);
ApiOperation? op;
Map<String, String>? p;
for (final o in CloudOperations.all) {
final m = o.match(r.method, r.path);
if (m != null) {
op = o;
p = m;
break;
}
}
if (op == null) {
return _problem(404, 'not-found', 'no route ${r.method} ${r.path}');
}
bind_impl.recordExercised('CloudOperations.${op.id}');
_User? who;
final auth = r.headers['authorization'];
if (auth != null && auth.startsWith('Bearer ')) {
final a = _access[auth.substring(7)];
if (a != null && a.$2.isAfter(clock.now())) who = _users[a.$1];
}
if (op.scope != 'anon' && who == null) {
return _problem(401, 'unauthenticated', 'sign in first');
}
final b = r.json is Map
? (r.json! as Map).cast<String, Object?>()
: const <String, Object?>{};
switch (op.id) {
case 'signUp':
final email = '${b['email']}'.toLowerCase();
if (_users.values.any((u) => u.email == email)) {
return _problem(409, 'conflict', 'e-mail already registered');
}
if ('${b['password']}'.length < 8) {
return _problem(
422,
'validation-failed',
'/password: at least 8 characters',
);
}
final u = _User(
uuidV7(),
email,
'${b['password']}',
b['displayName'] as String?,
);
_users[u.id] = u;
_mail(u, 'verify');
return _json(201, _session(u, '${b['client'] ?? 'native'}'));
case 'signIn' || 'createSession':
final u = _users.values
.where((u) => u.email == '${b['email']}'.toLowerCase())
.firstOrNull;
if (u == null || u.password != b['password']) {
return _problem(401, 'unauthenticated', 'wrong e-mail or password');
}
return _json(200, _session(u, '${b['client'] ?? 'native'}'));
case 'refreshSession':
final t = _refresh.remove(b['refreshToken']);
if (t == null) {
return _problem(401, 'unauthenticated', 'refresh token invalid');
}
return _json(200, _session(_users[t.$1]!, 'native', sessionId: t.$2));
case 'signOut' || 'deleteSession':
_access.remove(auth!.substring(7));
return FreedomeResponse(204);
case 'signOutAll':
_access.removeWhere((_, v) => v.$1 == who!.id);
_refresh.removeWhere((_, v) => v.$1 == who!.id);
return FreedomeResponse(204);
case 'verifyEmail':
final t = _tokens.remove(b['token']);
if (t == null || t.$1 != 'verify') {
return _problem(
422,
'validation-failed',
'/token: invalid or expired',
);
}
_users[t.$2]!.verified = true;
return FreedomeResponse(204);
case 'resendEmailVerification':
_mail(who!, 'verify');
return _json(202, {'status': 'accepted'});
case 'forgotPassword':
final u = _users.values
.where((u) => u.email == '${b['email']}'.toLowerCase())
.firstOrNull;
if (u != null) _mail(u, 'reset');
return _json(202, {'status': 'accepted'}); // no account enumeration
case 'resetPassword':
final t = _tokens.remove(b['token']);
if (t == null || t.$1 != 'reset') {
return _problem(
422,
'validation-failed',
'/token: invalid or expired',
);
}
_users[t.$2]!.password = '${b['password']}';
_refresh.removeWhere((_, v) => v.$1 == t.$2);
return FreedomeResponse(204);
case 'changePassword':
if (who!.password != b['currentPassword']) {
return _problem(
422,
'validation-failed',
'/currentPassword: wrong password',
);
}
who.password = '${b['password']}';
return FreedomeResponse(204);
case 'requestMagicLink':
final u = _users.values
.where((u) => u.email == '${b['email']}'.toLowerCase())
.firstOrNull;
if (u != null) _mail(u, 'magic');
return _json(202, {'status': 'accepted'});
case 'verifyMagicLink':
final t = _tokens.remove(b['token']);
if (t == null || t.$1 != 'magic') {
return _problem(401, 'unauthenticated', 'link invalid or expired');
}
return _json(200, _session(_users[t.$2]!, 'native'));
case 'signInWithProvider':
final idt = '${b['idToken']}';
if (!idt.startsWith('${p!['provider']}:')) {
return _problem(401, 'unauthenticated', 'id token not accepted');
}
final email =
'${idt.substring(p['provider']!.length + 1)}@${p['provider']}.example';
final u =
_users.values.where((u) => u.email == email).firstOrNull ??
(_User(uuidV7(), email, '', b['displayName'] as String?)
..verified = true);
_users[u.id] = u;
return _json(200, _session(u, 'native'));
case 'getAccount':
return _json(200, _account(who!));
case 'patchAccount':
if (b['displayName'] != null) who!.displayName = '${b['displayName']}';
if (b['locale'] != null) who!.locale = '${b['locale']}';
return _json(200, _account(who!));
case 'deleteAccount':
who!
..status = 'deleting'
..hardDeleteAfter = clock
.now()
.add(const Duration(days: 7))
.toUtc()
.toIso8601String();
return _json(202, _account(who));
case 'cancelAccountDeletion':
who!
..status = 'active'
..hardDeleteAfter = null;
return _json(200, _account(who));
case 'listAccountDevices':
return _json(200, {
'items': [
for (final d in _devices.values)
if (d['userId'] == who!.id) {...d}..remove('userId'),
],
'nextCursor': null,
});
case 'deleteAccountDevice':
final d = _devices.remove(p!['sessionId']);
if (d == null) return _problem(404, 'not-found', 'session');
_refresh.removeWhere((_, v) => v.$2 == p!['sessionId']);
return FreedomeResponse(204);
default:
return _problem(
501,
'legacy-unsupported',
'${op.id} not implemented by FakeAccountServer',
);
}
}