# 🚀 Building a Fullstack App with dart_frog and Flutter in a Monorepo - Part 6

In earlier sections of this tutorial series, we covered the fundamental stages for creating a to-do application with Flutter and Dart.

We've covered everything from bootstrapping an empty [Flutter](https://flutter.dev) project, importing necessary dependencies, setting up the folder structure, integrating the frontend with the backend, and implementing the Todo data sources, repositories, and view models.

In this part, we'll show you how to utilize dart frog to establish user authentication with [JSON Web Tokens (JWT)](https://jwt.io). By the end of this article, we will be able to:

* Implement user login and register.
    
* Create `User` model.
    
* Create and implement `UserRepository` and `UserDataSource`.
    
* Handle `UnauthorizedException`.
    
* Create a new authorization middleware.
    
* Implement user authentication using [JWT](https://jwt.io).
    
* Secure routes with `Authorization` headers.
    

Don't forget to check out the [GitHub](https://github.com/saileshbro/full_stack_todo_dart) repo for this article if you need any assistance along the way.

> Let's get started 🚀

---

# Overview 🔍

> 👀 Get a sneak peek of what's to come 🔮

When we're done, we should have an app that supports the following requests:

1. `/todos*` - Guard all to-do routes with `Authorization` header.
    

```bash
curl -s -L -X POST 'http://localhost:8080/todos' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY5ZWFkZTg3LWRhNWUtNDRjZC1iNTRlLTQ3NGE4NWQ4ZGVlNCIsIm5hbWUiOiJTYWlsZXNoIERhaGFsIiwiZW1haWwiOiJzYWlsZXNoYnJvQGdtYWlsLmNvbSIsImNyZWF0ZWRfYXQiOiIyMDIzLTAxLTIzVDIyOjU0OjM4Ljg2NDkxMloiLCJwYXNzd29yZCI6IiQyYSQxMCR6VjBTZlI3cUpHOHlCelJWWThtNkRlMWo0UUtHL2VRdXl6NzduQ1lBL1luZENtb1ZSbzB0RyIsImlhdCI6MTY3NDQ5Mzc3OX0.W36mHXKFrxZDhz0Hrxt0Cmrz3WNVexiAZe2KAjrWMaE' -H 'Content-Type: application/json' --data-raw '{
    "title":"this is a title asdf",
    "description":"Not so great description asdf"
}'
```

1. `/users/login` - Login user
    

```bash
curl -s -L -X POST 'http://localhost:8080/users/login' -H 'Content-Type: application/json' --data-raw '{
    "email":"saileshbro@gmail.com",
    "password":"6aMj@UBByu"
}'
```

1. `/users/signup` - Register new user
    

```bash
curl -s -L -X POST 'http://localhost:8080/users/signup' -H 'Content-Type: application/json' --data-raw '{
    "email":"saileshbro@gmail.com",
    "name":"Sailesh Dahal",
    "password":"6aMj@UBByu%7BzN^C9tMe#Te4b!4cJrXwwFi#HgKrQ&g&"
}'
```

---

# Updating Database 🛠️

> 💾 Upgrading our database to its full potential 🚀

To begin, we will create a new table called `users` and add a [`foreign key`](https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-foreign-key/) to our current `todos` table. This ensures that each to-do item is associated with a single user, and that data is accessible only to that person.

## Create `users` table

We will create a new `users` table with the following columns.

* `id` : unique `uuid`
    
* `name` : name of the user
    
* `email` : email of the user
    
* `password` : hashed password of the user
    
* `created_at` : timestamp of when the user was created
    

```sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users(
    id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email TEXT NOT NULL,
	  password TEXT NOT NULL,
    created_at timestamp default current_timestamp NOT NULL
);
```

> The [`uuid-ossp`](https://www.postgresql.org/docs/10/uuid-ossp.html) extension is used to generate unique IDs for each user.

## Updating `todos` table

We will update the `todos` table to include a foreign key to the `users` table.

```sql
TRUNCATE TABLE todos;
ALTER TABLE todos
    ADD COLUMN user_id uuid NOT NULL,
    ADD CONSTRAINT constraint_fk
        FOREIGN KEY (user_id)
            REFERENCES users (id)
            ON DELETE CASCADE;
```

> The [`ON DELETE CASCADE`](https://www.commandprompt.com/education/postgresql-delete-cascade-with-examples/) clause ensures that when a user is deleted, all their to-do items will be deleted.

Once we are done with the changes, we can proceed to create a user schema in our `models` package.

> More about [`CONSTRAINTS`](https://www.postgresql.org/docs/current/ddl-constraints.html) here.

---

# Create `User` model

> 🧑‍💼 Defining our User Model 📝

We will start by creating a `UserId` in our `typedefs` package.

## Create `UserId` type

In `typedefs` package, we will make some changes. We will rename the older `src/typedefs.dart` file to `src/todo_types.dart` and create a new `src/user_types.dart` file.

```dart
typedef UserId = String;
```

> 💡 Make sure to update the exports.

```dart
library typedefs;

export 'src/todo_types.dart';
export 'src/user_types.dart';
```

## Update folder structure

In `models` package, we will create a new `user.dart` file. Since we are creating models for a different feature, we will mode the todo specific models to a new `todo` folder.

```apache
.
├── create_todo_dto
│   └── create_todo_dto.dart
├── todo.dart
└── update_todo_dto
    └── update_todo_dto.dart
```

Also, make sure to export `create_todo_dto` and `update_todo_dto` from `todo.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:models/src/serializers/date_time_converter.dart';
import 'package:typedefs/typedefs.dart';

export './create_todo_dto/create_todo_dto.dart';
export './update_todo_dto/update_todo_dto.dart';

part 'todo.freezed.dart';
part 'todo.g.dart';
```

## Create `User` model

Now, we will create a `User` model in `src/user/user.dart` file.

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:models/src/serializers/date_time_converter.dart';
import 'package:typedefs/typedefs.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const factory User({
    required UserId id,
    required String name,
    required String email,
    @DateTimeConverter() required DateTime createdAt,
    @Default('') @JsonKey(includeToJson: false) String password,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
```

> We are ignoring the `password` field when serializing the model to JSON. This is because we don't want to send the password to the client.

Similarly, we will create `CreateUserDto` and `LoginUserDto` for creating and logging in users.

In `src/user/create_user_dto/create_user_dto.dart` file,

```dart
import 'package:either_dart/either.dart';
import 'package:exceptions/exceptions.dart';
import 'package:failures/failures.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'create_user_dto.freezed.dart';
part 'create_user_dto.g.dart';

@freezed
class CreateUserDto with _$CreateUserDto {
  factory CreateUserDto({
    required String name,
    required String email,
    required String password,
  }) = _CreateUserDto;

  factory CreateUserDto.fromJson(Map<String, dynamic> json) =>
      _$CreateUserDtoFromJson(json);

  static Either<ValidationFailure, CreateUserDto> validated(
    Map<String, dynamic> json,
  ) {
    try {
      final errors = <String, List<String>>{};
      final name = json['name'] as String? ?? '';
      final email = json['email'] as String? ?? '';
      final password = json['password'] as String? ?? '';
      if (name.isEmpty) {
        errors['name'] = ['Name is required'];
      }
      if (email.isEmpty) {
        errors['email'] = ['Email is required'];
      }
      if (!email.contains('@')) {
        errors['email'] = ['Email is invalid'];
      }
      if (password.isEmpty) {
        errors['password'] = ['Password is required'];
      }
      if (password.length < 6) {
        errors['password'] = ['Password must be at least 6 characters'];
      }

      if (errors.isEmpty) return Right(CreateUserDto.fromJson(json));
      throw BadRequestException(
        message: 'Validation failed',
        errors: errors,
      );
    } on BadRequestException catch (e) {
      return Left(
        ValidationFailure(
          message: e.message,
          errors: e.errors,
          statusCode: e.statusCode,
        ),
      );
    }
  }
}
```

Similarly, we will create a new `login_user_dto`.

```dart
import 'package:either_dart/either.dart';
import 'package:exceptions/exceptions.dart';
import 'package:failures/failures.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'login_user_dto.freezed.dart';
part 'login_user_dto.g.dart';

@freezed
class LoginUserDto with _$LoginUserDto {
  factory LoginUserDto({
    required String email,
    required String password,
  }) = _LoginUserDto;

  factory LoginUserDto.fromJson(Map<String, dynamic> json) =>
      _$LoginUserDtoFromJson(json);

  static Either<ValidationFailure, LoginUserDto> validated(
    Map<String, dynamic> json,
  ) {
    try {
      final errors = <String, List<String>>{};
      final email = json['email'] as String? ?? '';
      final password = json['password'] as String? ?? '';
      if (email.isEmpty) {
        errors['email'] = ['Email is required'];
      }
      if (password.isEmpty) {
        errors['password'] = ['Password is required'];
      }
      if (errors.isEmpty) return Right(LoginUserDto.fromJson(json));
      throw BadRequestException(
        message: 'Validation failed',
        errors: errors,
      );
    } on BadRequestException catch (e) {
      return Left(
        ValidationFailure(
          message: e.message,
          errors: e.errors,
          statusCode: e.statusCode,
        ),
      );
    }
  }
}
```

These data transfer objects will be used to send, receive and validate data from the client in the backend.

> 💡 Make sure to export `create_user_dto` and `login_user_dto` from `user.dart`, and `user.dart` from `models.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:models/src/serializers/date_time_converter.dart';
import 'package:typedefs/typedefs.dart';

export './create_user_dto/create_user_dto.dart';
export './login_user_dto/login_user_dto.dart';

part 'user.freezed.dart';
part 'user.g.dart';
```

```dart
library models;

export 'src/todo/todo.dart';
export 'src/user/user.dart';
```

> 💡 Make sure to run `flutter pub run build_runner build` to generate new files.

---

# Creating `UserDataSource`

> 📊 Building the foundation for user management with `UserDataSource` 🛠️

In `data_source` package, we will create a new interface called `UserDataSource` in `data_source/lib/src/user_data_source.dart`

```dart
import 'package:models/models.dart';
import 'package:typedefs/typedefs.dart';

abstract class UserDataSource {
  Future<User> getUserById(UserId id);

  Future<User> createUser(CreateUserDto user);

  Future<User> getUserByEmail(String email);
}
```

We will have methods to query and create users. We will do the implementation in our `backend`.

---

# Creating `UserRepository`

> 🔍 Designing our User Management System with UserRepository 🛠️

We will also create an interface called `UserRepository` which will return either a failure or valid data making use of `UserDataSource`.

In `repository` package, we will create a new interface called `UserRepository` in `repository/lib/src/user_repository.dart`

```dart
import 'package:either_dart/either.dart';
import 'package:failures/failures.dart';
import 'package:models/models.dart';
import 'package:typedefs/typedefs.dart';

abstract class UserRepository {
  Future<Either<Failure, User>> getUserById(UserId id);

  Future<Either<Failure, User>> createUser(CreateUserDto createUserDto);

  Future<Either<Failure, User>> loginUser(LoginUserDto loginUserDto);

  Future<Either<Failure, User>> getUserByEmail(String email);
}
```

We will have all the methods we need to create, query and login users. We will do the implementation in our backend.

> 💡 Make sure to export `UserRepository` from `repository.dart`

```dart
library repository;

export 'src/todo_repository.dart';
export 'src/user_repository.dart';
```

---

# Implementing `UserDataSource`

> 💻 Bringing `UserDataSource` to life 🔧

Now, we will implement `UserDataSource` in our backend. In `backend/lib/user/data_source/user_data_source_impl.dart`, we will create a new class called `UserDataSourceImpl` which will implement `UserDataSource`.

This is how an empty implementation of `UserDataSourceImpl` will look like.

```dart
import 'package:data_source/data_source.dart';
import 'package:models/models.dart';
import 'package:typedefs/typedefs.dart';

class UserDataSourceImpl implements UserDataSource {
  UserDataSourceImpl(this._databaseConnection);

  final DatabaseConnection _databaseConnection;
  Future<User> createUser(CreateUserDto user) {
    throw UnimplementedError();
  }

  @override
  Future<User> getUserByEmail(String email) {
    throw UnimplementedError();
  }

  @override
  Future<User> getUserById(UserId id) {
    throw UnimplementedError();
  }
}
```

## Implementing `createUser` method

Now, we will implement `createUser` method. We will use `DatabaseConnection` to connect to our database and create a new user from the dto we received.

```dart
  @override
  Future<User> createUser(CreateUserDto user) async {
    try {
      await _databaseConnection.connect();
      final result = await _databaseConnection.db.query(
        '''
        INSERT INTO users (name, email, password)
        VALUES (@name, @email, @password) RETURNING *
        ''',
        substitutionValues: user.toJson(),
      );
      if (result.affectedRowCount == 0) {
        throw const ServerException('Failed to create todo');
      }
      final userMap = result.first.toColumnMap();
      return User.fromJson(userMap);
    } on PostgreSQLException catch (e) {
      throw ServerException(e.message ?? 'Unexpected error');
    } finally {
      await _databaseConnection.close();
    }
  }
```

We will validate the DTO in the repository before passing it to the data source. The database will then be queried to generate a new user. We will throw a `ServerException` if the user is not created, else, we will return the user that was created.

## Implementing `getUserByEmail` method

```dart
  @override
  Future<User> getUserByEmail(String email) async {
    try {
      await _databaseConnection.connect();
      final result = await _databaseConnection.db.query(
        '''
        SELECT id, name, email, password, created_at
        FROM users WHERE email = @email
        ''',
        substitutionValues: {'email': email},
      );
      if (result.isEmpty) {
        throw const NotFoundException('User not found');
      }
      return User.fromJson(result.first.toColumnMap());
    } on PostgreSQLException catch (e) {
      throw ServerException(e.message ?? 'Unexpected error');
    } finally {
      await _databaseConnection.close();
    }
  }
```

Here, we will query the database for the user with the given email. If the user is not found, we will throw a `NotFoundException`. We will handle this exception in the `UserController` and return a `404` response.

## Implementing `getUserById` method

```dart
@override
  Future<User> getUserById(UserId id) async {
    try {
      await _databaseConnection.connect();
      final result = await _databaseConnection.db.query(
        'SELECT id, name, email, created_at FROM users WHERE id = @id',
        substitutionValues: {'id': id},
      );
      if (result.isEmpty) {
        throw const NotFoundException('User not found');
      }
      return User.fromJson(result.first.toColumnMap());
    } on PostgreSQLException catch (e) {
      throw ServerException(e.message ?? 'Unexpected error');
    } finally {
      await _databaseConnection.close();
    }
  }
```

We will query the database for the user with the given id. If the user is not found, we will throw a `NotFoundException`. We will handle this exception in the `UserController` and return a `404` response.

---

# Creating `PasswordHasherService`

> 🔑 Keeping user passwords safe with `PasswordHasherService` 🔒

We will create a new service to hash and validate hashed passwords before going on to the `UserRepository` implementation. The `CreateUserDto` with the hashed password will be passed to the `UserRepository`.

In `backend/lib/services/password_hasher_service.dart`, we will create a new class called `PasswordHasherService`.

We will use [`bcrypt`](https://pub.dev/packages/bcrypt) package to hash and verify the password.

```bash
flutter pub add bcrypt
```

```dart
import 'package:bcrypt/bcrypt.dart';

class PasswordHasherService {
  const PasswordHasherService();

  String hashPassword(String password) {
    return BCrypt.hashpw(password, BCrypt.gensalt());
  }

  bool checkPassword(String password, String hashedPassword) {
    return BCrypt.checkpw(password, hashedPassword);
  }
}
```

Now, we can pass this service as a dependency to `UserRepository` when we implement our repository.

---

# Implementing `UserRepository`

> 💻 Bringing `UserRepository` to life 🔧

Now, we will implement `UserRepository` in our backend. In `backend/lib/user/repository/user_repository_impl.dart`, we will create a new class called `UserRepositoryImpl` which will implement `UserRepository`.

```dart
import 'package:backend/services/password_hasher_service.dart';
import 'package:data_source/data_source.dart';
import 'package:either_dart/either.dart';
import 'package:failures/failures.dart';
import 'package:models/models.dart';
import 'package:repository/repository.dart';
import 'package:typedefs/typedefs.dart';

class UserRepositoryImpl implements UserRepository {
  UserRepositoryImpl(this.dataSource, this.passwordHasherService);

  final UserDataSource dataSource;
  final PasswordHasherService passwordHasherService;

  @override
  Future<Either<Failure, User>> createUser(CreateUserDto createUserDto) {
    throw UnimplementedError();
  }

  @override
  Future<Either<Failure, User>> getUserByEmail(String email) {
    throw UnimplementedError();
  }

  @override
  Future<Either<Failure, User>> getUserById(UserId id) {
    throw UnimplementedError();
  }

  @override
  Future<Either<Failure, User>> loginUser(LoginUserDto loginUserDto) {
    throw UnimplementedError();
  }
}
```

## Implementing `getUserByEmail` method

We will start by implementing `getUserByEmail`. We will use the data source's `dataSource.getUserByEmail` method to see if we get a `User` object. If we get a `User` object, we shall wrap it in a `Right` object and return it. If an error occurs, we will return a `Left` object along with a `ServerFailure` object.

```dart
  @override
  Future<Either<Failure, User>> getUserByEmail(String email) async {
    try {
      final user = await dataSource.getUserByEmail(email);
      return Right(user);
    } catch (e) {
      log(e.toString());
      return const Left(
        ServerFailure(
          message: 'User with this email does not exist',
          statusCode: HttpStatus.notFound,
        ),
      );
    }
  }
```

## Implementing `getUserById` method

Similarly, we will call `dataSource.getUserById` method and see whether we get a `User` object. If we get a `User` object, we shall wrap it in a `Right` object and return it. If an error occurs, we will return a `Left` object along with a `ServerFailure` object.

```dart
  @override
  Future<Either<Failure, User>> getUserById(UserId id) async {
    try {
      final res = await dataSource.getUserById(id);
      return Right(res);
    } on NotFoundException catch (e) {
      log(e.message);
      return Left(
        ServerFailure(
          message: e.message,
          statusCode: e.statusCode,
        ),
      );
    } on ServerException catch (e) {
      log(e.message);
      return Left(
        ServerFailure(message: e.message),
      );
    }
  }
```

## Implementing `loginUser` method

Now, we will implement `loginUser`. We will check if the user with the given email exists. If the user exists, we will check the password with `PasswordHasherService`. If they do not match, we will return a failure, else we will return the logged in user.

This method will be used by the `UserController` to create an access token for the user.

```dart
  @override
  Future<Either<Failure, User>> loginUser(LoginUserDto loginUserDto) async {
    try {
      final email = loginUserDto.email;
      final userExists = await getUserByEmail(email);
      if (userExists.isLeft) {
        throw const ServerException('Invalid email or password');
      }
      final user = userExists.right;
      final password = loginUserDto.password;
      final isPasswordCorrect =
          passwordHasherService.checkPassword(password, user.password);
      if (!isPasswordCorrect) {
        throw const ServerException('Invalid email or password');
      }
      return Right(user);
    } catch (e) {
      log(e.toString());
      return const Left(
        ServerFailure(
          message: 'Invalid email or password',
          statusCode: HttpStatus.unauthorized,
        ),
      );
    }
  }
```

## Implementing `createUser` method

Similarly, we will implement `createUser`. We will check if the user with the given email exists. If the user exists, we will return an failure, else we will create the user and return it.

```dart
  @override
  Future<Either<Failure, User>> createUser(CreateUserDto createUserDto) async {
    try {
      final userExists = await getUserByEmail(createUserDto.email);
      if (userExists.isRight) {
        throw const ServerException('Email already in use');
      }
      // dto is already validated in the controller
      // we will hash the password here
      final hashedPassword = passwordHasherService.hashPassword(
        createUserDto.password,
      );
      final user = await dataSource.createUser(
        createUserDto.copyWith(
          password: hashedPassword,
        ),
      );
      return Right(user);
    } on ServerException catch (e) {
      log(e.message);
      return Left(
        ServerFailure(message: e.message),
      );
    }
  }
```

---

# Implementing `JwtService`

> 🔐 Securing User Login with JWT 🔒

Now, once we have all the necessary implementation of the repository and the data source, we will implement `JwtService` before implementing our `UserController`. This service will be used to create a JWT token for the user when they log in or signup.

In `backend/lib/user/service/jwt_service.dart`, we will create a new class called `JwtService`. We will use [`dart_jsonwebtoken`](https://pub.dev/packages/dart_jsonwebtoken) package to create and verify JWT tokens.

```bash
flutter pub add dart_jsonwebtoken
```

Now, we will implement the `JwtService` class.

```dart
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:dotenv/dotenv.dart';

class JWTService {
  const JWTService(this._env);

  final DotEnv _env;

  String sign(Map<String, dynamic> payload) {
    final secret = _env['JWT_SECRET']!;
    final jwt = JWT(payload);
    return jwt.sign(SecretKey(secret));
  }

  Map<String, dynamic> verify(String token) {
    final secret = _env['JWT_SECRET']!;
    final jwt = JWT.verify(token, SecretKey(secret));
    return jwt.payload as Map<String, dynamic>;
  }
}
```

> 💡 NOTE: We are using `dotenv` package to get the `JWT_SECRET` from the `.env` file. 💡 Make sure you have added the `JWT_SECRET` to the `.env` file.

```apache
...
JWT_SECRET=verycomplexsupersecret
```

Once this is done, we will implement the `UserController`.

---

# Tidy Up Time 🧹

> Make Room for Improvement 🧠

## Remove duplicates

We have multiple routes that are not implemented and are returning the same response. We will remove these routes and create a new `Handler` to handle these requests.

We will create a new `Handler` called `notAllowedRequestHandler` in `backend/lib/request_handlers/not_allowed_request_handler.dart` to handle these requests.

```dart
Future<Response> notAllowedRequestHandler(RequestContext context) async {
  return Response.json(
    body: {'error': '👀 Looks like you are lost 🔦'},
    statusCode: HttpStatus.methodNotAllowed,
  );
}
```

Then, we will replace the routes with this handler.

* `backend/routes/index.dart`
    

```dart
import 'package:backend/request_handlers/not_allowed_request_handler.dart';
import 'package:dart_frog/dart_frog.dart';

Handler onRequest = notAllowedRequestHandler;
```

* `backend/routes/todos/index.dart`
    
* `backend/routes/todos/[id].dart`
    

```diff
+     return notAllowedRequestHandler(context);
-     return Response.json(
-       body: {'error': '👀 Looks like you are lost 🔦'},
-       statusCode: HttpStatus.methodNotAllowed,
-     );
```

## Refactor `HttpController`

Right now, if we have to implement `HttpController`, we will have to override all the methods. We will refactor the `HttpController` to make it optional to override all the methods.

we will create a new handler called `unimplementedHandler` in `backend/lib/request_handlers/unimplemented_handler.dart` to handle the unimplemented methods.

```dart
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';

Future<Response> unimplementedHandler([RequestContext? context]) async {
  return Response.json(
    body: {'error': '👀 Not implemented yet'},
    statusCode: HttpStatus.notImplemented,
  );
}
```

Then, we will refactor the `HttpController` to use this handler. In `backend/lib/controller/http_controller.dart`, we will change the methods to call and return `unimplementedHandler`.

```dart
abstract class HttpController {
  FutureOr<Response> index(Request request) => unimplementedHandler();

  FutureOr<Response> store(Request request) => unimplementedHandler();

  FutureOr<Response> show(Request request, String id) => unimplementedHandler();

  FutureOr<Response> update(Request request, String id) =>
      unimplementedHandler();

  FutureOr<Response> destroy(Request request, String id) =>
      unimplementedHandler();
}
```

---

# Implementing `UserController`

> Adding magic with `UserController` Implementation 🧙‍♀️

Now, we will implement `UserController` in `user/controller/user_controller.dart`.

```dart
import 'dart:async';

import 'package:backend/controller/http_controller.dart';
import 'package:backend/services/jwt_service.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:repository/repository.dart';

class UserController extends HttpController {
  UserController(this._repo, this._jwtService);

  final UserRepository _repo;
  final JWTService _jwtService;

  @override
  FutureOr<Response> store(Request request) async {
    throw UnimplementedError();
  }

  FutureOr<Response> login(Request request) async {
    throw UnimplementedError();
  }
}
```

We will override `store` method, which will responsible for creating and storing the user, and then another `login` method, to log in the user.

Also, we will create a new private method called `_signAndSendToken` which will take the user, and then sign and send the user along with the JWT token.

## Implement `_signAndSendToken` method

We will use this method once `login` and `store` method are successful and when we want to send a response to the user.

```dart
  Response _signAndSendToken(User user, [int? httpStatus]) {
    final token = _jwtService.sign(user.toJson());
    return Response.json(
      body: {
        'token': token,
        'user': user.toJson()..remove('password'),
      },
      statusCode: httpStatus ?? HttpStatus.ok,
    );
  }
```

## Implement `store` method

We will parse the JSON body, and validate the body with `CreateUserDto`. Once validated, we will call `repository.createUser` which will hash the password and create a new user if not already there.

Once this is done, we will call `_signAndSendToken` and return with `201` status code.

```dart
  @override
  FutureOr<Response> store(Request request) async {
    final parsedBody = await parseJson(request);
    if (parsedBody.isLeft) {
      return Response.json(
        body: {'message': parsedBody.left.message},
        statusCode: parsedBody.left.statusCode,
      );
    }
    final json = parsedBody.right;
    final createTodoDto = CreateUserDto.validated(json);
    if (createTodoDto.isLeft) {
      return Response.json(
        body: {
          'message': createTodoDto.left.message,
          'errors': createTodoDto.left.errors,
        },
        statusCode: createTodoDto.left.statusCode,
      );
    }
    final res = await _repo.createUser(createTodoDto.right);
    return res.fold(
      (left) => Response.json(
        body: {'message': left.message},
        statusCode: left.statusCode,
      ),
      (right) => _signAndSendToken(right, HttpStatus.created),
    );
  }
```

## Implement `login`

Similar to `store`, we will parse and verify `LoginUserDto`, and call `repository.loginUser` which will verify the password and return the user.

```dart
  FutureOr<Response> login(Request request) async {
    final parsedBody = await parseJson(request);
    if (parsedBody.isLeft) {
      return Response.json(
        body: {'message': parsedBody.left.message},
        statusCode: parsedBody.left.statusCode,
      );
    }
    final json = parsedBody.right;
    final loginUserDto = LoginUserDto.validated(json);
    if (loginUserDto.isLeft) {
      return Response.json(
        body: {
          'message': loginUserDto.left.message,
          'errors': loginUserDto.left.errors,
        },
        statusCode: loginUserDto.left.statusCode,
      );
    }
    final res = await _repo.loginUser(loginUserDto.right);
    return res.fold(
      (left) => Response.json(
        body: {'message': left.message},
        statusCode: left.statusCode,
      ),
      _signAndSendToken,
    );
  }
```

---

# Adding Dependency Injections 🧰

> Making sure our ducks are in a row 🦆

In our global middleware `backend/routes/_middleware.dart`, we will register the dependencies.

```dart
import 'package:backend/db/database_connection.dart';
import 'package:backend/services/jwt_service.dart';
import 'package:backend/services/password_hasher_service.dart';
import 'package:backend/user/controller/user_controller.dart';
import 'package:backend/user/data_source/user_data_source_impl.dart';
import 'package:backend/user/repositories/user_repository_impl.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:data_source/data_source.dart';
import 'package:dotenv/dotenv.dart';
import 'package:repository/repository.dart';

final env = DotEnv()..load();
final _db = DatabaseConnection(env);
final _userDs = UserDataSourceImpl(_db);
const _passwordHasher = PasswordHasherService();
final _userRepo = UserRepositoryImpl(_userDs, _passwordHasher);
final _jwtService = JWTService(env);
final _userController = UserController(_userRepo, _jwtService);

Handler middleware(Handler handler) {
  return handler
      .use(requestLogger())
      .use(provider<DatabaseConnection>((_) => _db))
      .use(provider<JWTService>((_) => _jwtService))
      .use(provider<UserDataSource>((_) => _userDs))
      .use(provider<UserRepository>((_) => _userRepo))
      .use(provider<UserController>((_) => _userController))
      .use(provider<PasswordHasherService>((_) => _passwordHasher));
}
```

---

# Setting up routes

> Connecting the dots 🔗️ - defining API endpoints

We will now setup the routes for `UserController` in `backend/routes/user` we will create three new files `index.dart`, `login.dart`, and `signup.dart`.

Doing this we will have three different routes:

* `/user` - `index.dart`
    
* `/user/login` - `login.dart`
    
* `/user/signup` - `signup.dart`
    

## `index.dart`

This route does nothing. We will just return `unimplementedHandler`.

```dart
import 'package:backend/request_handlers/not_allowed_request_handler.dart';
import 'package:dart_frog/dart_frog.dart';

Handler onRequest = notAllowedRequestHandler;
```

## `login.dart`

We will get the `UserController` from the `RequestContext` and call `login` method for `POST` requests.

```dart
import 'dart:async';

import 'package:backend/request_handlers/not_allowed_request_handler.dart';
import 'package:backend/user/controller/user_controller.dart';
import 'package:dart_frog/dart_frog.dart';

FutureOr<Response> onRequest(RequestContext context) {
  final userController = context.read<UserController>();
  if (context.request.method != HttpMethod.post) {
    return notAllowedRequestHandler(context);
  }
  return userController.login(context.request);
}
```

## `signup.dart`

Similar to `login.dart`, we will get the `UserController` from the `RequestContext` and call `store` method for `POST` requests.

```dart
import 'dart:async';

import 'package:backend/request_handlers/not_allowed_request_handler.dart';
import 'package:backend/user/controller/user_controller.dart';
import 'package:dart_frog/dart_frog.dart';

FutureOr<Response> onRequest(RequestContext context) {
  final userController = context.read<UserController>();
  if (context.request.method != HttpMethod.post) {
    return notAllowedRequestHandler(context);
  }
  return userController.store(context.request);
}
```

> 💡 NOTE: Recheck your `.env` file before testing the routes.

---

# Testing routes

> Testing Time 🧪 - Verify Your Routes!

Once we have set up the routes, we can test them using `curl` or `postman`.

## `/users/login`

### When the email and password match

* `REQUEST`
    

```bash
curl -s -L -X POST 'http://localhost:8080/users/login' -H 'Content-Type: application/json' --data-raw '{
    "email":"saileshbro@gmail.com",
    "password":"6aMj@UBByu%7BzN^C9tMe#Te4b!4cJrXwwFi#HgKrQ&g&ddNN6eHQ94vd5SuJtEc%7^H6L^xews8soG@R7GnW*RvfJVMaKEuBXNtVtbP5!3^qs*n!Z%87q8eRJmKFUHg"
}'
```

* `RESPONSE`
    

```json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjQ4ODY5Yjc4LWEwMjgtNGMwNS1hN2QzLTQ5OTQ2Mzk4NDI2NSIsIm5hbWUiOiJTYWlsZXNoIERhaGFsIiwiZW1haWwiOiJzYWlsZXNoQGdtYWlsLmNvbSIsImNyZWF0ZWRfYXQiOiIyMDIzLTAxLTMwVDA5OjU3OjQ5LjExODM2MVoiLCJwYXNzd29yZCI6IiQyYSQxMCRGSDVDc2N1Y0VuLlNwWlZmSWFtRGwuZzRMaDFhd2ltbVRMS05yU2NORW1qT25lSFZHOE4wLiIsImlhdCI6MTY3NTA2MDA0MH0.qvzLWgAEphlYqZztoBf7Bvag6hO1qkp44hUwl78CMVo",
  "user": {
    "id": "48869b78-a028-4c05-a7d3-499463984265",
    "name": "Sailesh Dahal",
    "email": "saileshbro@gmail.com",
    "created_at": "2023-01-30T09:57:49.118361Z"
  }
}
```

### When the email or password does not match.

* `REQUEST`
    

```bash
curl -s -L -X POST 'http://localhost:8080/users/login' -H 'Content-Type: application/json' --data-raw '{
    "email":"sa@gmail.com",
    "name":"Sailesh Dahal",
    "password":"6aMj@UBByu%7BzN^C9tMe#Te4b!4cJrXwwFi#HgKrQ&g&ddNN6eHQ94vd5SuJtEc%7^H6L^xews8soG@R7GnW*RvfJVMaKEuBXNtVtbP5!3^qs*n!Z%87q8eRJmKFUHg"
}'
```

* `RESPONSE`
    

```json
{ "message": "Invalid email or password" }
```

## `/users/signup`

### When the data is not valid

* `REQUEST`
    

```bash
curl -s -L -X POST 'http://localhost:8080/users/signup' -H 'Content-Type: application/json' --data-raw '{
    "email":"sailes@gmail.com",
    "password":"6aMj@UBByu"
}'
```

* `RESPONSE`
    

```json
{
  "message": "Validation failed",
  "errors": {
    "name": ["Name is required"]
  }
}
```

### When there is no user with the given email, it will create a new user and return the same response as `/users/login`.

* `REQUEST`
    

```bash
curl -s -L -X POST 'http://localhost:8080/users/signup' -H 'Content-Type: application/json' --data-raw '{
    "email":"sailesh12@gmail.com",
    "name":"Sailesh Dahal",
    "password":"6aMj123"
}'
```

* `RESPONSE`
    

```json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImM1ODE2YWQ0LTNkODctNDUzZC1hZmFkLTUwMzljY2YxZjdjNyIsIm5hbWUiOiJTYWlsZXNoIERhaGFsIiwiZW1haWwiOiJzYWlsZXNoMTJAZ21haWwuY29tIiwiY3JlYXRlZF9hdCI6IjIwMjMtMDEtMzBUMDY6Mjg6MzUuMzkyMjM4WiIsInBhc3N3b3JkIjoiJDJhJDEwJHRuTllGM3FHQmlPT1dKeHVacm1MaU91SHhBMU1HQlNMcmoxYS5ndHY2TTNCMHpmRW5Dc1dLIiwiaWF0IjoxNjc1MDYwMTE0fQ.aUpo9HXTFmdmkTsfGoTp0mcK0OJ_fFuwZArqyNFpKvQ",
  "user": {
    "id": "c5816ad4-3d87-453d-afad-5039ccf1f7c7",
    "name": "Sailesh Dahal",
    "email": "sailesh12@gmail.com",
    "created_at": "2023-01-30T06:28:35.392238Z"
  }
}
```

### When there is a user with the given email, it will return the following response.

* `REQUEST`
    

```bash
curl -s -L -X POST 'http://localhost:8080/users/signup' -H 'Content-Type: application/json' --data-raw '{
    "email":"sailesh12@gmail.com",
    "name":"Sailesh Dahal",
    "password":"6aMj123"
}'
```

* `RESPONSE`
    

```json
{
  "message": "Email already in use"
}
```

---

# Protecting Routes

> Locking Down: Secure Your Endpoints 🔒

Now, we can protect our routes by adding middleware to the routes. We will check for the `Authorization` header and verify the token. We can intercept all the requests going to `/todos/*` and check for the token.

We can do this by creating a new authorization middleware. We will create a new file `backend/lib/middlewares/authorization_middleware.dart`. Before that, let's create a new exception for unauthorized requests.

## Creating `UnauthorizedException`

When the token is not valid, we will throw an `UnauthorizedException` which will be handled by the `ExceptionHandlerMiddleware`.

In `exceptions` package, we will create a new exception called `UnauthorizedException` in `exceptions/lib/src/http_exception/unauthorized_exception.dart`

```dart
import 'dart:io';

import 'package:exceptions/src/http_exception/http_exception.dart';

class UnauthorizedException extends HttpException {
  const UnauthorizedException({
    String message = 'Unauthorized',
    this.errors = const {},
  }) : super(message, HttpStatus.unauthorized);

  final Map<String, List<String>> errors;
}
```

> 💡 Make sure to export the exception in `http_exception.dart` file.

```dart
export './bad_request_exception.dart';
export './not_found_exception.dart';
export './unauthorized_exception.dart';
```

> 💡 Make sure to run `flutter pub run build_runner build` to generate the code.

## Creating `AuthorizationMiddleware`

> 🔒 Securing Endpoints with AuthorizationMiddleware 🛡️

Now, we can create the `AuthorizationMiddleware` in `backend/lib/middlewares/authorization_middleware.dart`.

```dart
import 'dart:io';

import 'package:backend/db/database_connection.dart';
import 'package:backend/services/jwt_service.dart';
import 'package:backend/todo/controller/todo_controller.dart';
import 'package:backend/todo/data_source/todo_data_source_impl.dart';
import 'package:backend/todo/repositories/todo_repository_impl.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:data_source/data_source.dart';
import 'package:exceptions/exceptions.dart';
import 'package:models/models.dart';
import 'package:repository/repository.dart';

Handler authorizationMiddleware(Handler handler) {
  return (context) async {
    try {
      final request = context.request;
      final authHeader = request.headers[HttpHeaders.authorizationHeader] ?? '';
      final token = authHeader.replaceFirst('Bearer ', '');
      if (token.isEmpty) throw const UnauthorizedException();
      final jwtService = context.read<JWTService>();
      final decoded = jwtService.verify(token);
      final decodedUser = User.fromJson(decoded);
      final userRepo = context.read<UserRepository>();
      final user = await userRepo.getUserById(decodedUser.id);
      if (user.isLeft) throw const UnauthorizedException();
      context = _handleAuthDependencies(context, user.right);
      return handler(context);
    } on UnauthorizedException catch (e) {
      return Response.json(
        body: {'message': e.message},
        statusCode: e.statusCode,
      );
    }
  };
}

RequestContext _handleAuthDependencies(
  RequestContext context,
  User user,
) {
  late RequestContext updatedContext;
  updatedContext = context.provide<User>(() => user);
  return updatedContext;
}
```

Here, we are checking for the `Authorization` header and verifying the token. If the token is valid, we are adding the `User` object to the context, so that we can get the logged-in user later from the provider.

> If the token is invalid, we will return a `401` response.

## Updating auth dependencies

Now, since we will have to add a `user_id` whenever, we will create a todo, we will have to pass the logged-in user to the TodoDataSource. We will update the `TodoDataSourceImpl` to accept the `User` object from the constructor.

```dart
class TodoDataSourceImpl implements TodoDataSource {
  /// {@macro todo_data_source_impl}
  const TodoDataSourceImpl(this._databaseConnection, this.user);
  final DatabaseConnection _databaseConnection;

  final User user;
}
```

Also, we will update `createTodo` and `updateTodo` methods to add the `user_id`.

### Update `createTodo` method

```diff
  @override
  Future<Todo> createTodo(CreateTodoDto todo) async {
    try {
      await _databaseConnection.connect();
      final result = await _databaseConnection.db.query(
        '''
-       INSERT INTO todos (title, description, completed)
+       INSERT INTO todos (title, description, completed, user_id)
-       VALUES (@title, @description, @completed) RETURNING *
+       VALUES (@title, @description, @completed, @user_id) RETURNING *
        ''',
        substitutionValues: {
          'title': todo.title,
          'description': todo.description,
          'completed': false,
+         'user_id': _user.id,
        },
      );
      if (result.affectedRowCount == 0) {
        throw const ServerException('Failed to create todo');
      }
      final todoMap = result.first.toColumnMap();
      return Todo(
        id: todoMap['id'] as int,
+       userId: todoMap['user_id'] as String,
        title: todoMap['title'] as String,
        description: todoMap['description'] as String,
        createdAt: todoMap['created_at'] as DateTime,
      );
    } on PostgreSQLException catch (e) {
      throw ServerException(e.message ?? 'Unexpected error');
    } finally {
      await _databaseConnection.close();
    }
  }
```

### Update `updateTodo` method

```diff
      final result = await _databaseConnection.db.query(
        '''
        UPDATE todos
        SET title = COALESCE(@new_title, title),
            description = COALESCE(@new_description, description),
            completed = COALESCE(@new_completed, completed),
            updated_at = current_timestamp
        WHERE id = @id
+       AND user_id = @user_id
        RETURNING *
        ''',
        substitutionValues: {
          'id': id,
+         'user_id': _user.id,
          'new_title': todo.title,
          'new_description': todo.description,
          'new_completed': todo.completed,
        },
      );
```

### Update `Todo` model to add `userId`

We will also add the missing `userId` from `Todo` model.

```diff
  factory Todo({
    required TodoId id,
+   required UserId userId,
    required String title,
    @Default('') String description,
    @Default(false) bool completed,
    @DateTimeConverter() required DateTime createdAt,
    @DateTimeConverterNullable() DateTime? updatedAt,
  }) = _Todo;
```

Now, once this is done, we will update the `_handleAuthDependencies` method in `authorization_middleware.dart` as

```dart
RequestContext _handleAuthDependencies(
  RequestContext context,
  User user,
) {
  final db = context.read<DatabaseConnection>();
  final todoDs = TodoDataSourceImpl(db, user);
  final todoRepo = TodoRepositoryImpl(todoDs);
  final todoController = TodoController(todoRepo);
  late RequestContext updatedContext;
  updatedContext = context.provide<User>(() => user);
  updatedContext = updatedContext.provide<TodoController>(() => todoController);
  updatedContext = updatedContext.provide<TodoRepository>(() => todoRepo);
  updatedContext = updatedContext.provide<TodoDataSource>(() => todoDs);
  return updatedContext;
}
```

## Using `AuthorizationMiddleware`

> Applying `AuthorizationMiddleware` to routes

Let's create a new middleware in `backend/routes/todos/_middleware.dart` and add the following code.

```dart
import 'package:backend/middlewares/authorization_middleware.dart';
import 'package:dart_frog/dart_frog.dart';

Handler middleware(Handler handler) => authorizationMiddleware(handler);
```

Since this middleware lies inside `routes/todos`, all the `/todos/*` routes will be intercepted by this middleware. This means that all the `/todos/*` routes will be protected and if the user tries to access these routes without a valid token, it will return a `401` response.

---

# Testing protected `/todos/*` routes

> Putting updated routes to the Test 🧪

If we try to access the `/todos/*` routes without a valid token, it will return a `401` response. We can test this by running the following command.

## GET /todos

* `REQUEST`
    

```bash
curl -s -L -X GET 'http://localhost:8080/todos'
```

* `RESPONSE`
    

```json
{
  "message": "Unauthorized"
}
```

* `REQUEST`
    

```bash
curl -s -L -X GET 'http://localhost:8080/todos' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY5ZWFkZTg3LWRhNWUtNDRjZC1iNTRlLTQ3NGE4NWQ4ZGVlNCIsIm5hbWUiOiJTYWlsZXNoIERhaGFsIiwiZW1haWwiOiJzYWlsZXNoYnJvQGdtYWlsLmNvbSIsImNyZWF0ZWRfYXQiOiIyMDIzLTAxLTIzVDIyOjU0OjM4Ljg2NDkxMloiLCJwYXNzd29yZCI6IiQyYSQxMCR6VjBTZlI3cUpHOHlCelJWWThtNkRlMWo0UUtHL2VRdXl6NzduQ1lBL1luZENtb1ZSbzB0RyIsImlhdCI6MTY3NDQ5Mzc3OX0.W36mHXKFrxZDhz0Hrxt0Cmrz3WNVexiAZe2KAjrWMaE'
```

* `RESPONSE`
    

```json
[
  {
    "id": 76,
    "user_id": "69eade87-da5e-44cd-b54e-474a85d8dee4",
    "title": "this is a title asdf",
    "description": "Not so great description asdf",
    "completed": false,
    "created_at": "2023-01-23T22:56:31.686023Z",
    "updated_at": "2023-01-30T04:08:06.349549Z"
  }
]
```

## POST /todos

* `REQUEST`
    

```bash
curl -s -L -X POST 'http://localhost:8080/todos' -H 'Content-Type: application/json' --data-raw '{
    "title":"this is a title asdf",
    "description":"Not so great description asdf"
}'
```

* `RESPONSE`
    

```json
{
  "message": "Unauthorized"
}
```

* `REQUEST`
    

```bash
curl -s -L -X POST 'http://localhost:8080/todos' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY5ZWFkZTg3LWRhNWUtNDRjZC1iNTRlLTQ3NGE4NWQ4ZGVlNCIsIm5hbWUiOiJTYWlsZXNoIERhaGFsIiwiZW1haWwiOiJzYWlsZXNoYnJvQGdtYWlsLmNvbSIsImNyZWF0ZWRfYXQiOiIyMDIzLTAxLTIzVDIyOjU0OjM4Ljg2NDkxMloiLCJwYXNzd29yZCI6IiQyYSQxMCR6VjBTZlI3cUpHOHlCelJWWThtNkRlMWo0UUtHL2VRdXl6NzduQ1lBL1luZENtb1ZSbzB0RyIsImlhdCI6MTY3NDQ5Mzc3OX0.W36mHXKFrxZDhz0Hrxt0Cmrz3WNVexiAZe2KAjrWMaE' -H 'Content-Type: application/json' --data-raw '{
    "title":"this is a title asdf",
    "description":"Not so great description asdf"
}'
```

* `RESPONSE`
    

```json
{
  "id": 79,
  "user_id": "69eade87-da5e-44cd-b54e-474a85d8dee4",
  "title": "this is a title asdf",
  "description": "Not so great description asdf",
  "completed": false,
  "created_at": "2023-01-30T07:10:10.102358Z",
  "updated_at": null
}
```

Similarly, we can test the other routes as well 🥳

---

🎉 Congrats, we've made it to the end of Part 6! 🚀 We've successfully added user authentication with JWT to our [dart\_frog](https://dartfrog.vgv.dev/) backend.

It's been an incredible adventure, and we've gone a long way since Part 1. Using Dart and Flutter, we created a full-stack to-do application. But our task does not end there. We haven't addressed testing yet in this course, but it's an important aspect of developing any program. Let us know in the comments if you'd want to learn more about testing your code in all of the modules we've written in this series.

As always, you can refer back to the GitHub repo for this tutorial at [https://github.com/saileshbro/full\_stack\_todo\_dart](https://github.com/saileshbro/full_stack_todo_dart) if you need any help. Don't forget to give it a ⭐ and help spread the word about the initiative. Also, if you get stuck or want assistance, please submit an issue or, better yet, contribute a pull request.

Thank you for joining me on this journey; let's create even more wonderful applications together. Have fun coding! 💻
