Building User + Auth Service for Forum-Based Web using Redis (Part 2)
Tags: redis, beginners, webdev, tutorial
Published: 2022-07-17 00:04:55

Are you ready to build a Forum-Based Website? This is your chance! Let's go! We will start from User Service and Auth Service which is quite a tight coupling.

## Design

### Database

We will only have User data to be stored in MongoDB.

![User Service Database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8jfdudtll3yuv48d3qiy.jpeg)

### Architecture That Need to Consider

We will need Auth Service for Update/Delete User. We use `Bearer Token` and verify it.

![Update/Delete User](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4767omb6fgk76rl0g3qc.jpeg)

## User Service Base

### Install Dependencies

0. Please make sure we've already in the `app/user-service` directory.
1. Install `MongoDB.Driver`. We will use this dependency to connect to MongoDB. Using this command to install: `dotnet add UserService package MongoDB.Driver --version 2.16.1`.
2. Install `Isopoh.Cryptography.Argon2`. We will use this to hash the user password. Using this command to install: `dotnet add UserService package Isopoh.Cryptography.Argon2 --version 1.1.12`
3. Install `Redis.OM`. We will use this to connect Redis Stack. `dotnet add UserService package Redis.OM --version 0.1.9`
4. Install AutoMapper. Using this commands:

    ```bash
    dotnet add UserService package AutoMapper --version 11.0.1
    dotnet add UserService package AutoMapper.Extensions.Microsoft.DependencyInjection --version 11.0.0
    ```
Your `UserService.csproj` will become like this.

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="AutoMapper" Version="11.0.1" />
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
    <PackageReference Include="Isopoh.Cryptography.Argon2" Version="1.1.12" />
    <PackageReference Include="MongoDB.Driver" Version="2.16.1" />
    <PackageReference Include="Redis.OM" Version="0.1.9" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
  </ItemGroup>

</Project>
```

### Connect User Service with MongoDB

Basically, we will have CRUD functions using MongoDB.

1. Prepare Settings Model. Create file `UserService/Model/ForumApiDatabaseSettings.cs`.

    ```csharp
    namespace UserService.Model;
    
    public class ForumApiDatabaseSettings
    {
        public string ConnectionString { get; set; } = null!;
        public string DatabaseName { get; set; } = null!;
        public string UsersCollectionName { get; set; } = null!;
    }
    ```
2. Add config to `appsettings.json`. You will need to config 

    ```json
    // ... other config
    "ForumApiDatabase": {
        "ConnectionString": "mongodb://root:secretpass@localhost:27017",
        "DatabaseName": "ForumApi",
        "UsersCollectionName": "Users"
      },
    ```

3. Add `ForumApiDatabaseSettings` for DI at `Program.cs`.

    ```csharp
    // Add services to the container.
    builder.Services.Configure<ForumApiDatabaseSettings>(builder.Configuration.GetSection("ForumApiDatabase"));
    ```

4. Create a Users Model to store the data. Add to `UserService/Model/Users.cs`

    ```csharp
    namespace UserService.Model;
    
    using System.ComponentModel.DataAnnotations;
    using System.Text.Json.Serialization;
    
    using MongoDB.Bson.Serialization.Attributes;
    using MongoDB.Bson.Serialization.IdGenerators;
    
    using Redis.OM.Modeling;
    
    [Document(StorageType = StorageType.Json, Prefixes = new[] { "Users" }, IndexName = "users-idx")]
    public class Users
    {
        [BsonId(IdGenerator = typeof(CombGuidGenerator))]
        [RedisIdField]
        public Guid Id { get; set; }
        [Required]
        [Indexed]
        public string? Name { get; set; }
        [Required]
        [Indexed]
        public string? Email { get; set; }
        [Required]
        [JsonIgnore]
        public string? Password { get; set; }
    }
    ```

5. Create a Repository to store data in MongoDB.

    a. File `UserService/Repository/IUserRepository.cs`.
    
    ```csharp
    namespace UserService.Repository;
    
    using UserService.Model;
    
    public interface IUserRepository
    {
        Task<List<Users>> GetUsers();
        Task<Users?> GetUserById(Guid id);
        Task<Users?> GetUserByEmail(string email);
        Task<Users?> NewUser(Users user);
        Task<Users> UpdateUser(Guid id, Users user);
        Task<Users?> UpdateUserPassword(Guid id, Users user);
        Task DeleteUser(Guid id);
    }
    ```

    b. File `UserService/Repository/UserRepository.cs`.

    ```csharp
    namespace UserService.Repository;
    
    using System.Text.Json;
    
    using Isopoh.Cryptography.Argon2;
    
    using Microsoft.Extensions.Options;
    
    using MongoDB.Driver;
    
    using UserService.Model;
    
    public class UserRepository : IUserRepository
    {
    
        private readonly IMongoCollection<Users> _usersCollection;
        private readonly ILogger<UserRepository> _logger;

        public UserRepository(IOptions<ForumApiDatabaseSettings> forumApiDatabaseSettings, ILogger<UserRepository> logger)
        {
            _logger = logger;
            var mongoClient = new MongoClient(forumApiDatabaseSettings.Value.ConnectionString);
            var mongoDatabase = mongoClient.GetDatabase(forumApiDatabaseSettings.Value.DatabaseName);
            _usersCollection = mongoDatabase.GetCollection<Users>(forumApiDatabaseSettings.Value.UsersCollectionName);
        }
    
        public async Task<List<Users>> GetUsers()
        {
            return await _usersCollection.Find(_ => true).ToListAsync();
        }
    
        public async Task<Users?> GetUserById(Guid id)
        {
            return await _usersCollection.Find(x => x.Id == id).FirstOrDefaultAsync();
        }
    
        public async Task<Users?> GetUserByEmail(string email)
        {
            return await _usersCollection.Find(x => x.Email == email).FirstOrDefaultAsync();
        }
    
        public async Task<Users?> NewUser(Users user)
        {
            if (user.Password == null)
            {
                _logger.LogDebug("Didn't provide user Password when Create User. Data: {}", JsonSerializer.Serialize(user));
                return null;
            }
            var hashPassword = Argon2.Hash(user.Password);
            user.Password = hashPassword;
            await _usersCollection.InsertOneAsync(user);
            return user;
        }
    
        public async Task<Users> UpdateUser(Guid id, Users user)
        {
            user.Id = id;
            await _usersCollection.ReplaceOneAsync(x => x.Id == id, user, new ReplaceOptions()
            {
                IsUpsert = false,
            });
            return user;
        }
    
        public async Task<Users?> UpdateUserPassword(Guid id, Users user)
        {
            if (user.Password == null)
            {
                return null;
            }
            user.Id = id;
            var hashPassword = Argon2.Hash(user.Password);
            user.Password = hashPassword;
            await _usersCollection.ReplaceOneAsync(x => x.Id == id, user, new ReplaceOptions()
            {
                IsUpsert = false,
            });
            return user;
        }
    
        public async Task DeleteUser(Guid id)
        {
            await _usersCollection.DeleteOneAsync(x => x.Id == id);
        }
    }
    ```

    c. Add `IUserRepository.cs` and `UserRepository.cs` to [DI](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-6.0) (Dependency Injection) in `Program.cs`.

    ```csharp
    builder.Services.AddSingleton<IUserRepository, UserRepository>();
    ```

### Create Services to Build the Functional

We will continue the function of User Service. Our function will write/update the cache after creating/updating the user. The read function will look at the cache first, after that fall back to MongoDB.

#### Initiate the Service

* File `UserService/Service/IUserServices.cs`

```csharp
namespace UserService.Service;

using UserService.Model;

public interface IUserServices
{
    Task<List<Users>> GetUsers();
    Task<Users?> GetUserById(Guid id);
    Task<Users?> GetUserByEmail(string email);
    Task<IResult> NewUser(Users user);
    Task<IResult> UpdateUser(Guid id, UserUpdate user, HttpRequest request);
    Task<IResult> UpdateUserPassword(Guid id, UserPassword user, HttpRequest request);
    Task<IResult> DeleteUser(Guid id, HttpRequest request);
}
```

* File `UserService/Service/UserServices.cs`

```csharp
namespace UserService.Service;

using System;
using System.Text.Json;
using System.Threading.Tasks;

using Redis.OM;
using Redis.OM.Searching;

using UserService.Model;
using UserService.Repository;

public class UserServices : IUserServices
{
    private readonly IUserRepository _userRepository;
    private readonly RedisCollection<Users> _userCache;
    private readonly RedisConnectionProvider _provider;
    private readonly ILogger<UserServices> _logger;

    public UserServices(IUserRepository userRepository, RedisConnectionProvider provider, ILogger<UserServices> logger)
    {
        _logger = logger;
        _userRepository = userRepository;
        _provider = provider;
        _userCache = (RedisCollection<Users>)provider.RedisCollection<Users>();
    }

    // another code will be here
}
```

**Note**: We will consider Auth Service connection using API call after we've done with Auth Service.

#### Read Function

We will have some functions. There are `Read All`, `Read By Id`, `Read By Email`.

* For `Read All` users data, we will directly take from DynamoDB. Add this to `UserService/Service/UserServices.cs`.

```csharp
    public async Task<List<Users>> GetUsers()
    {
        return await _userRepository.GetUsers();
    }
```

* For `Read By Email` and `Read By Id`, we will prioritize cache data, if we don't have data in the Redis, we will continue to check in MongoDB.

```csharp
    public async Task<Users?> GetUserByEmail(string email)
    {
        var existing = await _userCache.Where(x => x.Email == email).FirstOrDefaultAsync();
        if (existing != null)
        {
            return existing;
        }
        return await _userRepository.GetUserByEmail(email);
    }

    public async Task<Users?> GetUserById(Guid id)
    {
        var existing = await _userCache.Where(x => x.Id == id).FirstOrDefaultAsync();
        if (existing != null)
        {
            return existing;
        }
        return await _userRepository.GetUserById(id);
    }
```

#### Create Function

```csharp
    public async Task<IResult> NewUser(Users user)
    {
        if (user.Email == null)
        {
            _logger.LogDebug("User Didn't Provide Email. Data: {}", JsonSerializer.Serialize(user));
            return Results.BadRequest(new { Message = "Please Provide Email" });
        }
        var existing = await _userRepository.GetUserByEmail(user.Email);
        if (existing != null)
        {
            _logger.LogDebug("Found Existing Email. Data: {}", JsonSerializer.Serialize(user));
            return Results.BadRequest(new { Message = "Users Exists" });
        }
        var createdUser = await _userRepository.NewUser(user);
        if (createdUser == null)
        {
            _logger.LogDebug("Failed To Create User. Data: {}", JsonSerializer.Serialize(user));
            return Results.BadRequest(new { Message = "Failed When Create User" });
        }
        await _userCache.InsertAsync(createdUser);
        return Results.Json(new { Message = "Created", User = createdUser });
    }
```

#### Update Function

For update, we will separate to update common details of the User and update the password.

```csharp
public async Task<IResult> UpdateUser(Guid id, UserUpdate user, HttpRequest request)
    {
        // we will uncomment this later
        // var verifyResult = await verifyUserAccess(request, id);
        // if (verifyResult != null)
        // {
        //     return verifyResult;
        // }
        var existing = await _userRepository.GetUserById(user.Id);
        if (existing == null)
        {
            return Results.NotFound(new { Message = "User Not Found" });
        }
        existing.Name = user.Name;
        var updatedUser = await _userRepository.UpdateUser(id, existing);
        var existingCache = await _userCache.Where(x => x.Id == id).FirstOrDefaultAsync();
        if (existingCache == null)
        {
            await _userCache.InsertAsync(updatedUser);
        }
        else
        {
            existingCache.Name = updatedUser.Name;
            await _userCache.Update(existingCache);
        }
        return Results.Json(new { Message = "Updated", User = existing });
    }

    public async Task<IResult> UpdateUserPassword(Guid id, UserPassword user, HttpRequest request)
    {
        // we will uncomment this later
        // var verifyResult = await verifyUserAccess(request, id);
        // if (verifyResult != null)
        // {
        //     return verifyResult;
        // }
        var existing = await _userRepository.GetUserById(user.Id);
        if (existing == null)
        {
            return Results.NotFound(new { Message = "User Not Found" });
        }
        existing.Password = user.Password;
        var updatedUser = await _userRepository.UpdateUserPassword(id, existing);
        if (updatedUser == null)
        {
            return Results.BadRequest(new { Message = "Failed to Update Password" });
        }
        var existingCache = await _userCache.Where(x => x.Id == id).FirstOrDefaultAsync();
        if (existingCache == null)
        {
            await _userCache.InsertAsync(updatedUser);
        }
        else
        {
            existingCache.Password = updatedUser.Password;
            await _userCache.Update(existingCache);
        }
        return Results.Json(new { Message = "Updated", User = updatedUser });
    }
```

#### Delete User

```csharp
    public async Task<IResult> DeleteUser(Guid id, HttpRequest request)
    {
        // we will uncomment this later
        // var verifyResult = await verifyUserAccess(request, id);
        // if (verifyResult != null)
        // {
        //     return verifyResult;
        // }
        var existing = await _userRepository.GetUserById(id);
        if (existing == null)
        {
            return Results.NotFound(new { Message = "User Not Found" });
        }
        await _userRepository.DeleteUser(id);
        await _userCache.Delete(existing);
        return Results.Json(new { Message = "Deleted" });
    }
```

#### Update Minimal API Route

Next, we will update our `Program.cs` to have API with those functions.

* Prepare `IndexCreationService`. Create the file `UserService/HostedService/IndexCreationService`.

```csharp
namespace UserService.HostedServices;

using Microsoft.Extensions.Logging;

using Redis.OM;

using UserService.Model;

public class IndexCreationService : IHostedService
{
    private readonly RedisConnectionProvider _provider;
    private readonly ILogger<IndexCreationService> _logger;

    public IndexCreationService(RedisConnectionProvider provider, ILogger<IndexCreationService> logger)
    {
        _provider = provider;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogDebug("Create Index {}", typeof(Users));
        var result = await _provider.Connection.CreateIndexAsync(typeof(Users));
        _logger.LogDebug("Create Index {} Result: {}", typeof(Users), result);

    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}
```

* Prepare the Model for Request Data.

  * Create File `UserService/Model/UserCreation.cs`

    ```csharp
    namespace UserService.Model;
    
    using System.ComponentModel.DataAnnotations;
    
    public class UserCreation
    {
        [Required]
        public Guid Id { get; set; }
        [Required]
        public string Name { get; set; } = null!;
        [Required]
        public string Email { get; set; } = null!;
        [Required]
        public string Password { get; set; } = null!;
    }
    ```

  * Create File `UserService/Model/UserPassword.cs`

    ```csharp
    namespace UserService.Model;
    
    using System.ComponentModel.DataAnnotations;
    
    public class UserPassword
    {
        [Required]
        public Guid Id { get; set; }
        [Required]
        public string Password { get; set; } = null!;
    }
    ```

  * Create File `UserService/Model/UserUpdate.cs`

    ```csharp
    namespace UserService.Model;
    
    using System.ComponentModel.DataAnnotations;
    
    public class UserUpdate
    {
        [Required]
        public Guid Id { get; set; }
        [Required]
        public string Name { get; set; } = null!;
    }
    ```

  * Create File `UserService/Model/ById.cs`

    ```csharp
    
    namespace UserService.Model;
    
    using System.ComponentModel.DataAnnotations;
    
    class ById
    {
        [Required]
        public Guid Id { get; set; }
    }
    ```

  * Create File for AutoMapper. Create at `UserService/Model/UserProfile.cs`

    ```csharp
    namespace UserService.Model;
    
    using AutoMapper;
    
    public class UserProfile : Profile
    {
        public UserProfile()
        {
            CreateMap<UserCreation, Users>();
        }
    }
    ```

* Update file `Program.cs`

```csharp
using AutoMapper;

using Microsoft.AspNetCore.Mvc;

using Redis.OM;

using UserService.HostedServices;
using UserService.Model;
using UserService.Repository;
using UserService.Service;



var builder = WebApplication.CreateBuilder(args);

// ... other codes

// Add Connection to Redis
builder.Services.AddSingleton(new RedisConnectionProvider(builder.Configuration["RedisConnectionString"]));
// Add User Services to DI
builder.Services.AddScoped<IUserServices, UserServices>();
// Add Index Creator
builder.Services.AddHostedService<IndexCreationService>();
// add Automapper
builder.Services.AddAutoMapper(typeof(UserProfile));

// ... other codes

// API Mapper
app.MapGet("/users", async ([FromServices] IUserServices userServices) =>
{
    return await userServices.GetUsers();
})
.WithName("GetUsers");

app.MapGet("/users/{id}", async ([FromServices] IUserServices userServices, Guid id) =>
{
    return await userServices.GetUserById(id);
})
.WithName("GetUserById");

app.MapGet("/userByEmail/{email}", async ([FromServices] IUserServices userServices, string email) =>
{
    return await userServices.GetUserByEmail(email);
})
.WithName("GetUserByEmail");

app.MapPost("/users", async ([FromServices] IUserServices userServices, [FromServices] IMapper mapper, [FromBody] UserCreation userCreation) =>
{
    var user = mapper.Map<Users>(userCreation);
    return await userServices.NewUser(user);
})
.WithName("CreateUser");

app.MapPut("/users", async ([FromServices] IUserServices userServices, [FromBody] UserUpdate user, HttpRequest req) =>
{
    return await userServices.UpdateUser(user.Id, user, req);
})
.WithName("UpdateUser");

app.MapPut("/users/password", async ([FromServices] IUserServices userServices, [FromBody] UserPassword user, HttpRequest req) =>
{
    return await userServices.UpdateUserPassword(user.Id, user, req);
})
.WithName("UpdateUserPassword");

app.MapDelete("/users", async ([FromServices] IUserServices userServices, [FromBody] ById data, HttpRequest req) =>
{
    return await userServices.DeleteUser(data.Id, req);
})
.WithName("DeleteUser");
```

> The User Service ready! Yey! You may try to run the project and call the API using `curl`, `Postman`, or other tools. You may check the final result [here](https://github.com/bervProject/forum-api-microservices/tree/step-2).

## Auth Service

### Install Dependencies

* Install `mongodb`, `redis-om`, `argon2`, `jsonwebtoken`, and `winston`.

```bash
yarn add argon2@^0.28.5 jsonwebtoken@^8.5.1 mongodb@^4.7.0 redis-om@^0.3.5 winston@^3.8.1
yarn add --dev @types/jsonwebtoken@^8.5.8
```

Result:

```json
  "dependencies": {
    "argon2": "^0.28.5",
    "express": "^4.18.1",
    "jsonwebtoken": "^8.5.1",
    "mongodb": "^4.7.0",
    "redis-om": "^0.3.5",
    "winston": "^3.8.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.13",
    "@types/jsonwebtoken": "^8.5.8",
    "@types/node": "^14.11.2",
    "@typescript-eslint/eslint-plugin": "^5.29.0",
    "@typescript-eslint/parser": "^5.29.0",
    "eslint": "^8.18.0",
    "ts-node-dev": "^2.0.0",
    "typescript": "^4.7.4"
  }
```

### Setup Logger using Winston

* File `src/logger.ts`

```ts
import { createLogger, format, transports } from "winston";
const { combine, timestamp, printf } = format;

const myFormat = printf(({ level, message, timestamp, ...meta }) => {
  const service = meta.service;
  delete meta.service;
  const otherMeta = JSON.stringify(meta);
  return `[${service}] ${timestamp} [${level}] ${message}. Metadata: ${otherMeta}`;
});

const logger = createLogger({
  level: "info",
  format: combine(timestamp(), myFormat),
  defaultMeta: { service: "auth-service" },
  transports: [new transports.Console()],
});

export default logger;
```

* Log each requests (`src/index.ts`)

```ts
import logger from "./logger";

// other codes

app.use((req, res, next) => {
  const requestMeta = {
    body: req.body,
    headers: req.headers,
    ip: req.ip,
    method: req.method,
    url: req.url,
    hostname: req.hostname,
    query: req.query,
    params: req.params,
  };

  logger.info("Getting Request", requestMeta);
  next();
});
```

### Setup Store Token in Redis

* Setup Redis Client. File `src/services/redis.ts`.

```ts
import { Client } from "redis-om";
const connectionString = process.env.REDIS_CONNECTION_STRING;

export const getClient = async () => {
  return await new Client().open(connectionString);
};
```

* File `src/entities/token.ts`

```ts
import { Entity, Schema } from "redis-om";
import { getClient } from "../services/redis";

class Token extends Entity {}

const tokenSchema = new Schema(Token, {
  token: { type: "string" },
});

export const getTokenRepository = async () => {
  const client = await getClient();
  return client.fetchRepository(tokenSchema);
};
```

* File `src/repositories/user.ts`

```ts
import { client as mongoClient } from "../services/mongo";

export const getUser = async (email: string) => {
  try {
    await mongoClient.connect();
    const database = mongoClient.db(process.env.MONGO_DB_NAME || "ForumApi");
    const users = database.collection(
      process.env.MONGO_USER_COLLECTION || "Users"
    );
    const query = { Email: email };
    return await users.findOne(query);
  } finally {
    mongoClient.close();
  }
};
```

* Setup index (`src/index.ts`)

```ts

import { getTokenRepository } from "./entities/token";

// other codes

async function initDB() {
  const tokenRepository = await getTokenRepository();
  await tokenRepository.createIndex();
}

// other codes

app.listen(port, () => {
  initDB().then(() => {
    logger.info(`NODE_ENV: ${process.env.NODE_ENV}`);
    logger.info(`Server listen on port ${port}`);
  });
});
```

### Main Function of Auth Service

#### Login

* Prepase constant data. File `src/constants.ts`.

```ts
const constants = {
  defaultExpired: "6h",
  defaultExpiredRefresh: "24h",
  defaultExpiredSecond: 86400,
};

export default constants;
```

* Create Jwt Manager. File `src/services/token.ts`.

```ts
import jwt from "jsonwebtoken";
import constants from "../constants";

const secretToken = process.env.AUTH_SECRET || "auth_secret_123";
const refreshTokenSecret = process.env.REFRESH_SECRET || "refresh_secret_123";

type TokenType = "ACCESS_TOKEN" | "REFRESH_TOKEN";

const getSecret = (tokenType: TokenType) => {
  return tokenType == "ACCESS_TOKEN" ? secretToken : refreshTokenSecret;
};

const getExp = (tokenType: TokenType) => {
  return tokenType == "ACCESS_TOKEN"
    ? constants.defaultExpired
    : constants.defaultExpiredRefresh;
};

export const generateToken = (
  {
    id,
    name,
    email,
  }: {
    id: string;
    name: string;
    email: string;
  },
  tokenType: TokenType
) => {
  const secret = getSecret(tokenType);
  return jwt.sign({ id, name, email }, secret, {
    expiresIn: getExp(tokenType),
  });
};

export const verifyToken = (token: string, tokenType: TokenType) => {
  const secret = getSecret(tokenType);
  return jwt.verify(token, secret);
};

export const decode = (token: string) => {
  return jwt.decode(token);
};
```

* Prepare MongoDB Connection. Why we need this? We will read the user data directly to MongoDB. File `src/services/mongo.ts`

```ts
import { MongoClient } from "mongodb";

const connectionString = process.env.MONGO_CONNECTION_STRING;
export const client = new MongoClient(connectionString || "");
```

* Prepare the function in `src/routes/auth.ts`.

```ts
import { Router } from "express";
import argon2 from "argon2";
import { JwtPayload } from "jsonwebtoken";
import { getTokenRepository } from "../entities/token";
import { generateToken, verifyToken } from "../services/token";
import { getUser } from "../repositories/user";
import constants from "../constants";
import logger from "../logger";

const failedLoginMessage = {
  message: "Failed to Login",
};

const failedLogoutMessage = {
  message: "Failed to Logout",
};

const commonFailed = {
  message: "Failed",
};

type SuccessLogin = {
  message: string;
  accessToken?: string;
  refreshToken?: string;
};

export const router = Router();

router.post("/login", async (req, res) => {
  const email = req.body.email;
  const password = req.body.password;
  if (!email || !password) {
    res.statusCode = 401;
    res.json(failedLoginMessage);
    return;
  }
  const existingUser = await getUser(email);
  if (existingUser == null) {
    res.statusCode = 401;
    res.json(failedLoginMessage);
    return;
  }
  const existingPass = existingUser.Password;
  try {
    const verify = await argon2.verify(existingPass, password);
    if (!verify) {
      res.statusCode = 401;
      res.json(failedLoginMessage);
      return;
    }
  } catch (err) {
    logger.error("Error when verify token", err);
    res.statusCode = 401;
    res.json(failedLoginMessage);
    return;
  }

  const userID = existingUser._id.toString("hex");
  const payload = {
    id: userID,
    name: existingUser.Name,
    email: existingUser.Email,
  };
  const accessToken = generateToken(payload, "ACCESS_TOKEN");
  const refreshToken = generateToken(payload, "REFRESH_TOKEN");
  const tokenRepository = await getTokenRepository();
  const createdToken = await tokenRepository.createAndSave({
    token: refreshToken,
  });
  await tokenRepository.expire(
    createdToken.entityId,
    constants.defaultExpiredSecond
  );
  const successMessage: SuccessLogin = {
    message: "Success",
  };
  successMessage["accessToken"] = accessToken;
  successMessage["refreshToken"] = refreshToken;
  res.json(successMessage);
});

// other codes
```

* Setup auth routes in `src/index.ts`.

```ts
import { router as authRouter } from "./routes/auth";

// other codes
app.use("/auth", authRouter);

```

#### Logout

Update `src/routes.auth.ts`.

```ts
router.post("/logout", async (req, res) => {
  const token = req.body.token;
  if (!token) {
    res.statusCode = 400;
    res.json(failedLogoutMessage);
    return;
  }
  const tokenRepository = await getTokenRepository();
  const tokenId = await tokenRepository
    .search()
    .where("token")
    .equals(token)
    .return.firstId();
  if (tokenId == null) {
    res.statusCode = 400;
    res.json(failedLogoutMessage);
    return;
  }
  tokenRepository.remove(tokenId);
  res.json({ message: "Success Logout" });
});
```

#### Verify

This function will be needed by User Service. Update at `src/routes/auth.ts`.

```ts
router.post("/verify", async (req, res) => {
  const token = req.body.token;
  if (!token) {
    res.statusCode = 400;
    res.json(commonFailed);
    return;
  }
  try {
    const payload = verifyToken(token, "ACCESS_TOKEN");
    const data = payload as JwtPayload;
    const { id } = data;
    res.json({
      message: "OK",
      id,
    });
    return;
  } catch (err) {
    logger.error("Error when verify token", err);
    res.statusCode = 401;
    res.json(commonFailed);
    return;
  }
});
```

#### Refresh

We will use this to get a new `accessToken`. Update at `src/routes/auth.ts`.

```ts
router.post("/refresh", async (req, res) => {
  const token = req.body.token;
  if (!token) {
    res.statusCode = 400;
    res.json(commonFailed);
    return;
  }
  try {
    const payload = verifyToken(token, "REFRESH_TOKEN");
    const tokenRepository = await getTokenRepository();
    const tokenId = await tokenRepository
      .search()
      .where("token")
      .equals(token)
      .return.firstId();
    if (tokenId == null) {
      res.statusCode = 400;
      res.json(failedLogoutMessage);
      return;
    }
    const data = payload as JwtPayload;
    const { id, name, email } = data;
    res.json({
      accessToken: generateToken({ id, name, email }, "ACCESS_TOKEN"),
    });
    return;
  } catch (err) {
    logger.error("Error when verify token", err);
    res.statusCode = 401;
    res.json(commonFailed);
    return;
  }
});
```

> Now we're ready to integrate with User Service!

## Update User Service

We will integrate User Service with Auth Service so our Update/Delete will check the Bearer Token.

* Create `UserService/Service/IAuthService.cs`.

```csharp
namespace UserService.Service;

public interface IAuthService
{
    Task<(bool, Guid)> Verify(string bearerToken);
}
```


* Create `UserService/Service/AuthService.cs`. We will call the AuthService using HttpClient.

```csharp
using Microsoft.Extensions.Options;

using Newtonsoft.Json;

using UserService.Model;

namespace UserService.Service;

public class AuthService : IAuthService
{
    private readonly HttpClient _httpClient;
    private readonly string _authVerify;
    private readonly ILogger<AuthService> _logger;

    public AuthService(IHttpClientFactory httpClientFactory, IOptions<AuthServiceSettings> authServiceSetting, ILogger<AuthService> logger)
    {
        _httpClient = httpClientFactory.CreateClient();
        _authVerify = authServiceSetting.Value.AuthServiceVerify;
        _logger = logger;
    }

    public async Task<(bool, Guid)> Verify(string bearerToken)
    {
        if (string.IsNullOrEmpty(bearerToken))
        {
            _logger.LogDebug("Getting empty bearer.");
            return (false, Guid.Empty);
        }
        var splitted = bearerToken.Split(' ');
        if (splitted.Length != 2)
        {
            _logger.LogDebug("The Bearer Not Valid.");
            return (false, Guid.Empty);
        }

        var jsonBody = JsonContent.Create(new { token = splitted[1] });

        var response = await _httpClient.PostAsync(_authVerify, jsonBody);

        if (!response.IsSuccessStatusCode)
        {
            _logger.LogDebug("Getting non 200 response from Auth Service. Response: {}", response.StatusCode);
            return (false, Guid.Empty);
        }

        Guid userId = Guid.Empty;

        if (response.Content is object && response.Content.Headers.ContentType != null && response.Content.Headers.ContentType.MediaType == "application/json")
        {
            var contentStream = await response.Content.ReadAsStreamAsync();
            using var streamReader = new StreamReader(contentStream);
            using var jsonReader = new JsonTextReader(streamReader);

            JsonSerializer serializer = new JsonSerializer();

            try
            {
                var responseData = serializer.Deserialize<VerifyId>(jsonReader);
                if (responseData != null)
                {
                    var byteArray = Utils.StringToByteArrayFastest(responseData.Id);
                    userId = new Guid(byteArray);
                }
            }
            catch (JsonReaderException)
            {
                _logger.LogDebug("Invalid JSON from Auth Service.");
            }
        }
        return (true, userId);

    }

    private class VerifyId
    {
        public string Id { get; set; } = null!;
    }
}
```

* Setup `Program.cs` to have HttpClient DI and AuthService in DI.

```csharp
builder.Services.AddHttpClient();
builder.Services.AddTransient<IAuthService, AuthService>();
```

* Update `UserServices.cs`. Don't forget to uncomment some codes from the previous step when we create the User Service.

```csharp
public class UserServices : IUserServices
{
    private readonly IUserRepository _userRepository;
    private readonly IAuthService _authService;
    private readonly RedisCollection<Users> _userCache;
    private readonly RedisConnectionProvider _provider;
    private readonly ILogger<UserServices> _logger;

    public UserServices(IUserRepository userRepository, IAuthService authService, RedisConnectionProvider provider, ILogger<UserServices> logger)
    {
        _logger = logger;
        _userRepository = userRepository;
        _authService = authService;
        _provider = provider;
        _userCache = (RedisCollection<Users>)provider.RedisCollection<Users>();
    }

   // other codes
   private async Task<IResult?> verifyUserAccess(HttpRequest request, Guid userId)
    {
        var bearerToken = request.Headers.Authorization.ToString();
        var (success, id) = await _authService.Verify(bearerToken);
        if (!success)
        {
            return Results.Unauthorized();
        }
        if (id != userId)
        {
            return Results.Unauthorized();
        }
        return null;
    }

}
```

## Finally!

We've learned to use Redis as Cache Data of User Data and also storing Jwt Token data. If you want to try your APIs, you may use this [Postman Collection](https://www.getpostman.com/collections/5dd5670dc18183937bcd). You may compare your code with [this](https://github.com/bervProject/forum-api-microservices/tree/step-2). Other files that are different are optional to improve access to the codes.

{% embed https://github.com/bervProject/forum-api-microservices/pull/6 %}

### Redis Feature Highlight

> We can easily implement RedisJSON using Redis-OM. For example User data, we can implement to store User data to RedisJSON using `[Document(StorageType = StorageType.Json, Prefixes = new[] { "Users" }, IndexName = "users-idx")]`. It's also easy for Redis-OM for Node.

```ts
import { Entity, Schema } from "redis-om";

class Token extends Entity {}

const tokenSchema = new Schema(Token, {
  token: { type: "string" },
});
```

How about searching using RediSearch? It's also easy! For example to search token.

```ts
const tokenId = await tokenRepository
      .search()
      .where("token")
      .equals(token)
      .return.firstId();
```

We also can check your data in your Redis server using Redis Insight.

![Redis Insight](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nln5iji0b0nii86fg4lk.png)
 

## Repository

{% github bervproject/forum-api-microservices %}

## Additional

* This article used Redis.OM version 0.1.9, there is a breaking change when using 0.2.0. If you wish to use 0.2.0, you can see this PR (Pull Request) to see the changes. I will use 0.2.0 for Thread Service so that you can know the difference.

{% embed https://github.com/bervProject/forum-api-microservices/pull/7 %}

## Thank you for reading

For the next step, we will learn to build Thread Services. After that, we will learn to build the User Interface (Frontend side). So, let's go!

![Thanks GIF](https://media.giphy.com/media/5A4H6rLzx2hWLOd0Lw/giphy.gif)