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.
Architecture That Need to Consider
We will need Auth Service for Update/Delete User. We use Bearer Token
and verify it.
User Service Base
Install Dependencies
- Please make sure we've already in the
app/user-service
directory. - 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
. - 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
- Install
Redis.OM
. We will use this to connect Redis Stack.dotnet add UserService package Redis.OM --version 0.1.9
-
Install AutoMapper. Using this commands:
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.
<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.
-
Prepare Settings Model. Create file
UserService/Model/ForumApiDatabaseSettings.cs
.
namespace UserService.Model; public class ForumApiDatabaseSettings { public string ConnectionString { get; set; } = null!; public string DatabaseName { get; set; } = null!; public string UsersCollectionName { get; set; } = null!; }
-
Add config to
appsettings.json
. You will need to config
// ... other config "ForumApiDatabase": { "ConnectionString": "mongodb://root:secretpass@localhost:27017", "DatabaseName": "ForumApi", "UsersCollectionName": "Users" },
-
Add
ForumApiDatabaseSettings
for DI atProgram.cs
.
// Add services to the container. builder.Services.Configure<ForumApiDatabaseSettings>(builder.Configuration.GetSection("ForumApiDatabase"));
-
Create a Users Model to store the data. Add to
UserService/Model/Users.cs
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; } }
-
Create a Repository to store data in MongoDB.
a. File
UserService/Repository/IUserRepository.cs
.
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
.
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
andUserRepository.cs
to DI (Dependency Injection) inProgram.cs
.
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
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
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 toUserService/Service/UserServices.cs
.
public async Task<List<Users>> GetUsers()
{
return await _userRepository.GetUsers();
}
- For
Read By Email
andRead By Id
, we will prioritize cache data, if we don't have data in the Redis, we will continue to check in MongoDB.
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
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.
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
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 fileUserService/HostedService/IndexCreationService
.
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
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
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
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
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
namespace UserService.Model; using AutoMapper; public class UserProfile : Profile { public UserProfile() { CreateMap<UserCreation, Users>(); } }
- Create File
Update file
Program.cs
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.
Auth Service
Install Dependencies
- Install
mongodb
,redis-om
,argon2
,jsonwebtoken
, andwinston
.
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:
"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
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
)
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
.
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
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
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
)
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
.
const constants = {
defaultExpired: "6h",
defaultExpiredRefresh: "24h",
defaultExpiredSecond: 86400,
};
export default constants;
- Create Jwt Manager. File
src/services/token.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
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
.
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
.
import { router as authRouter } from "./routes/auth";
// other codes
app.use("/auth", authRouter);
Logout
Update src/routes.auth.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
.
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
.
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
.
namespace UserService.Service;
public interface IAuthService
{
Task<(bool, Guid)> Verify(string bearerToken);
}
- Create
UserService/Service/AuthService.cs
. We will call the AuthService using HttpClient.
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.
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.
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. You may compare your code with this. Other files that are different are optional to improve access to the codes.
Init User + Auth Module #6
- [x] MongoDB
- [x] Redis OM
- [x] Repository
- [x] Service
- [x] Authorization
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.
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.
const tokenId = await tokenRepository
.search()
.where("token")
.equals(token)
.return.firstId();
We also can check your data in your Redis server using Redis Insight.
Repository
bervProject / forum-api-microservices
Forum API Microservices
forum-api-microservices
Forum API Microservices
Directory Structure
-
/app : Microservices Source Code
- Currently we have Auth Service, User Service, and Thread Service.
- docker-compose.yml : Containerize MongoDB & Redis. Will help for development.
Microservices Development
- You will need to copy or modify
docker-compose.yml
to ignore the deployment of microservices. - Run Redis & MongoDB using
docker compose up -d
. - Go to the microservice you want to update and read the README.md of each directory to understand how to run them.
Development
- Build Images of Microservices:
docker compose build
- Run all:
docker compose up -d
Software Architecture
License
MIT
MIT License
Copyright (c) 2022 Bervianto Leo Pratama's Personal Projects
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute,
…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.
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!
Top comments (0)