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',
      );
  }
}