diff --git a/README.md b/README.md index cc206438..c8ca4c0d 100644 --- a/README.md +++ b/README.md @@ -32,93 +32,95 @@ This course is designed with a lot of details, so that everyone, even with very ## Course videos +- Lecture #0: [Setup development environment on Windows: WSL2 + Go + VSCode + Docker + Make + Sqlc](https://www.youtube.com/watch?v=TtCfDXfSw_0&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) + ### Section 1: Working with database [Postgres] -- Lecture #1: [Design DB schema and generate SQL code with dbdiagram.io](https://www.youtube.com/watch?v=rx6CPDK_5mU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=1) -- Lecture #2: [Install & use Docker + Postgres + TablePlus to create DB schema](https://www.youtube.com/watch?v=Q9ipbLeqmQo&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=2) -- Lecture #3: [How to write & run database migration in Golang](https://www.youtube.com/watch?v=0CYkrGIJkpw&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=3) -- Lecture #4: [Generate CRUD Golang code from SQL | Compare db/sql, gorm, sqlx & sqlc](https://www.youtube.com/watch?v=prh0hTyI1sU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=4) -- Lecture #5: [Write unit tests for database CRUD with random data in Golang](https://www.youtube.com/watch?v=phHDfOHB2PU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=5) -- Lecture #6: [A clean way to implement database transaction in Golang](https://www.youtube.com/watch?v=gBh__1eFwVI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=6) -- Lecture #7: [DB transaction lock & How to handle deadlock in Golang](https://www.youtube.com/watch?v=G2aggv_3Bbg&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=7) -- Lecture #8: [How to avoid deadlock in DB transaction? Queries order matters!](https://www.youtube.com/watch?v=qn3-5wdOfoA&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=8) -- Lecture #9: [Deeply understand transaction isolation levels & read phenomena in MySQL & PostgreSQL](https://www.youtube.com/watch?v=4EajrPgJAk0&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=9) -- Lecture #10: [Setup Github Actions for Golang + Postgres to run automated tests](https://www.youtube.com/watch?v=3mzQRJY1GVE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=10) +- Lecture #1: [Design DB schema and generate SQL code with dbdiagram.io](https://www.youtube.com/watch?v=rx6CPDK_5mU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #2: [Install & use Docker + Postgres + TablePlus to create DB schema](https://www.youtube.com/watch?v=Q9ipbLeqmQo&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #3: [How to write & run database migration in Golang](https://www.youtube.com/watch?v=0CYkrGIJkpw&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #4: [Generate CRUD Golang code from SQL | Compare db/sql, gorm, sqlx & sqlc](https://www.youtube.com/watch?v=prh0hTyI1sU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #5: [Write unit tests for database CRUD with random data in Golang](https://www.youtube.com/watch?v=phHDfOHB2PU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #6: [A clean way to implement database transaction in Golang](https://www.youtube.com/watch?v=gBh__1eFwVI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #7: [DB transaction lock & How to handle deadlock in Golang](https://www.youtube.com/watch?v=G2aggv_3Bbg&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #8: [How to avoid deadlock in DB transaction? Queries order matters!](https://www.youtube.com/watch?v=qn3-5wdOfoA&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #9: [Deeply understand transaction isolation levels & read phenomena in MySQL & PostgreSQL](https://www.youtube.com/watch?v=4EajrPgJAk0&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #10: [Setup Github Actions for Golang + Postgres to run automated tests](https://www.youtube.com/watch?v=3mzQRJY1GVE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) ### Section 2: Building RESTful HTTP JSON API [Gin] -- Lecture #11: [Implement RESTful HTTP API in Go using Gin](https://www.youtube.com/watch?v=n_Y_YisgqTw&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=11) -- Lecture #12: [Load config from file & environment variables in Go with Viper](https://www.youtube.com/watch?v=n5p8HkO6bnE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=12) -- Lecture #13: [Mock DB for testing HTTP API in Go and achieve 100% coverage](https://www.youtube.com/watch?v=rL0aeMutoJ0&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=13) -- Lecture #14: [Implement transfer money API with a custom params validator](https://www.youtube.com/watch?v=5q_wsashJZA&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=14) -- Lecture #15: [Add users table with unique & foreign key constraints in PostgreSQL](https://www.youtube.com/watch?v=D4VtNC3vQUs&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=15) -- Lecture #16: [How to handle DB errors in Golang correctly](https://www.youtube.com/watch?v=mJ8b5GcvoxQ&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=16) -- Lecture #17: [How to securely store passwords? Hash password in Go with Bcrypt!](https://www.youtube.com/watch?v=B3xnJI2lHmc&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=17) -- Lecture #18: [How to write stronger unit tests with a custom gomock matcher](https://www.youtube.com/watch?v=DuzBE0jKOgE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=18) -- Lecture #19: [Why PASETO is better than JWT for token-based authentication?](https://www.youtube.com/watch?v=nBGx-q52KAY&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=19) -- Lecture #20: [How to create and verify JWT & PASETO token in Golang](https://www.youtube.com/watch?v=Oi4FHDGILuY&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=20) -- Lecture #21: [Implement login user API that returns PASETO or JWT access token in Go](https://www.youtube.com/watch?v=lnHbZ9GOGAs&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=21) -- Lecture #22: [Implement authentication middleware and authorization rules in Golang using Gin](https://www.youtube.com/watch?v=Pw8fVBRS4jA&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=22) +- Lecture #11: [Implement RESTful HTTP API in Go using Gin](https://www.youtube.com/watch?v=n_Y_YisgqTw&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #12: [Load config from file & environment variables in Go with Viper](https://www.youtube.com/watch?v=n5p8HkO6bnE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #13: [Mock DB for testing HTTP API in Go and achieve 100% coverage](https://www.youtube.com/watch?v=rL0aeMutoJ0&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #14: [Implement transfer money API with a custom params validator](https://www.youtube.com/watch?v=5q_wsashJZA&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #15: [Add users table with unique & foreign key constraints in PostgreSQL](https://www.youtube.com/watch?v=D4VtNC3vQUs&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #16: [How to handle DB errors in Golang correctly](https://www.youtube.com/watch?v=mJ8b5GcvoxQ&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #17: [How to securely store passwords? Hash password in Go with Bcrypt!](https://www.youtube.com/watch?v=B3xnJI2lHmc&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #18: [How to write stronger unit tests with a custom gomock matcher](https://www.youtube.com/watch?v=DuzBE0jKOgE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #19: [Why PASETO is better than JWT for token-based authentication?](https://www.youtube.com/watch?v=nBGx-q52KAY&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #20: [How to create and verify JWT & PASETO token in Golang](https://www.youtube.com/watch?v=Oi4FHDGILuY&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #21: [Implement login user API that returns PASETO or JWT access token in Go](https://www.youtube.com/watch?v=lnHbZ9GOGAs&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #22: [Implement authentication middleware and authorization rules in Golang using Gin](https://www.youtube.com/watch?v=Pw8fVBRS4jA&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) ### Section 3: Deploying the application to production [Kubernetes + AWS] -- Lecture #23: [Build a minimal Golang Docker image with a multistage Dockerfile](https://www.youtube.com/watch?v=p1dwLKAxUxA&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=23) -- Lecture #24: [How to use docker network to connect 2 stand-alone containers](https://www.youtube.com/watch?v=VcFnqQarpjI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=24) -- Lecture #25: [How to write docker-compose file and control service start-up orders with wait-for.sh](https://www.youtube.com/watch?v=jf6sQsz0M1M&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=25) -- Lecture #26: [How to create a free tier AWS account](https://www.youtube.com/watch?v=4UqN1P8pIkM&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=26) -- Lecture #27: [Auto build & push docker image to AWS ECR with Github Actions](https://www.youtube.com/watch?v=3M4MPmSWt9E&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=27) -- Lecture #28: [How to create a production DB on AWS RDS](https://www.youtube.com/watch?v=0EaG3T4Q5fQ&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=28) -- Lecture #29: [Store & retrieve production secrets with AWS secrets manager](https://www.youtube.com/watch?v=3i1mQ_Ye8jE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=29) -- Lecture #30: [Kubernetes architecture & How to create an EKS cluster on AWS](https://www.youtube.com/watch?v=TxnCMhYhqRU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=30) -- Lecture #31: [How to use kubectl & k9s to connect to a kubernetes cluster on AWS EKS](https://www.youtube.com/watch?v=hwMevai3_wQ&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=31) -- Lecture #32: [How to deploy a web app to Kubernetes cluster on AWS EKS](https://www.youtube.com/watch?v=PH-Mcd0Rs1w&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=32) -- Lecture #33: [Register a domain name & set up A-record using Route53](https://www.youtube.com/watch?v=-JF2ukmW3i8&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=33) -- Lecture #34: [How to use Ingress to route traffics to different services in Kubernetes](https://www.youtube.com/watch?v=lBrqP6FkNsU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=34) -- Lecture #35: [Automatic issue TLS certificates in Kubernetes with Let's Encrypt](https://www.youtube.com/watch?v=nU4FTjrgSKI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=35) -- Lecture #36: [Automatic deploy to Kubernetes with Github Action](https://www.youtube.com/watch?v=GVY-zze0V_U&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=36) +- Lecture #23: [Build a minimal Golang Docker image with a multistage Dockerfile](https://www.youtube.com/watch?v=p1dwLKAxUxA&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #24: [How to use docker network to connect 2 stand-alone containers](https://www.youtube.com/watch?v=VcFnqQarpjI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #25: [How to write docker-compose file and control service start-up orders with wait-for.sh](https://www.youtube.com/watch?v=jf6sQsz0M1M&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #26: [How to create a free tier AWS account](https://www.youtube.com/watch?v=4UqN1P8pIkM&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #27: [Auto build & push docker image to AWS ECR with Github Actions](https://www.youtube.com/watch?v=3M4MPmSWt9E&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #28: [How to create a production DB on AWS RDS](https://www.youtube.com/watch?v=0EaG3T4Q5fQ&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #29: [Store & retrieve production secrets with AWS secrets manager](https://www.youtube.com/watch?v=3i1mQ_Ye8jE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #30: [Kubernetes architecture & How to create an EKS cluster on AWS](https://www.youtube.com/watch?v=TxnCMhYhqRU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #31: [How to use kubectl & k9s to connect to a kubernetes cluster on AWS EKS](https://www.youtube.com/watch?v=hwMevai3_wQ&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #32: [How to deploy a web app to Kubernetes cluster on AWS EKS](https://www.youtube.com/watch?v=PH-Mcd0Rs1w&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #33: [Register a domain name & set up A-record using Route53](https://www.youtube.com/watch?v=-JF2ukmW3i8&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #34: [How to use Ingress to route traffics to different services in Kubernetes](https://www.youtube.com/watch?v=lBrqP6FkNsU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #35: [Automatic issue TLS certificates in Kubernetes with Let's Encrypt](https://www.youtube.com/watch?v=nU4FTjrgSKI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #36: [Automatic deploy to Kubernetes with Github Action](https://www.youtube.com/watch?v=GVY-zze0V_U&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) ### Section 4: Advanced Backend Topics [Sessions + gRPC] -- Lecture #37: [How to manage user session with refresh token - Golang](https://www.youtube.com/watch?v=rT20ylRLm5U&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=37) -- Lecture #38: [Generate DB documentation page and schema SQL dump from DBML](https://www.youtube.com/watch?v=dGfVwsPr-IU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=38) -- Lecture #39: [Introduction to gRPC](https://www.youtube.com/watch?v=mRGnA3wPxMM&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=39) -- Lecture #40: [Define gRPC API and generate Go code with protobuf](https://www.youtube.com/watch?v=mVWgEmyAhvM&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=40) -- Lecture #41: [How to run a golang gRPC server and call its API](https://www.youtube.com/watch?v=BkfBJIS0_ro&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=41) -- Lecture #42: [Implement gRPC API to create and login users in Go](https://www.youtube.com/watch?v=7xiWqyZW9lE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=42) -- Lecture #43: [gRPC gateway: write code once, serve both gRPC & HTTP requests](https://www.youtube.com/watch?v=3FfDH3d0aHs&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=43) -- Lecture #44: [How to extract info from gRPC metadata](https://www.youtube.com/watch?v=Sno10WQ21Zs&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=44) -- Lecture #45: [Automatic generate & serve Swagger docs from Go server](https://www.youtube.com/watch?v=Uwkxxee7tvk&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=45) -- Lecture #46: [Embed static frontend files inside Golang backend server's binary](https://www.youtube.com/watch?v=xNgOIm86N5Q&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=46) -- Lecture #47: [Validate gRPC parameters and send human/machine friendly response](https://www.youtube.com/watch?v=CxZ9hMtmZtc&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=47) -- Lecture #48: [Run DB migrations directly inside Golang code](https://www.youtube.com/watch?v=TG43cMpaxlI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=48) -- Lecture #49: [Partial update DB record with SQLC nullable parameters](https://www.youtube.com/watch?v=I2sbw1PzzW0&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=49) -- Lecture #50: [Build gRPC update API with optional parameters](https://www.youtube.com/watch?v=ygqSHIEc8sc&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=50) -- Lecture #51: [Add authorization to protect gRPC API](https://www.youtube.com/watch?v=_jqNs3d99ps&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=51) -- Lecture #52: [Write structured logs for gRPC APIs](https://www.youtube.com/watch?v=tTAxLGrDmPo&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=52) -- Lecture #53: [How to write HTTP logger middleware in Go](https://www.youtube.com/watch?v=Lbiz-PZNiU0&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=53) +- Lecture #37: [How to manage user session with refresh token - Golang](https://www.youtube.com/watch?v=rT20ylRLm5U&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #38: [Generate DB documentation page and schema SQL dump from DBML](https://www.youtube.com/watch?v=dGfVwsPr-IU&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #39: [Introduction to gRPC](https://www.youtube.com/watch?v=mRGnA3wPxMM&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #40: [Define gRPC API and generate Go code with protobuf](https://www.youtube.com/watch?v=mVWgEmyAhvM&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #41: [How to run a golang gRPC server and call its API](https://www.youtube.com/watch?v=BkfBJIS0_ro&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #42: [Implement gRPC API to create and login users in Go](https://www.youtube.com/watch?v=7xiWqyZW9lE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #43: [gRPC gateway: write code once, serve both gRPC & HTTP requests](https://www.youtube.com/watch?v=3FfDH3d0aHs&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #44: [How to extract info from gRPC metadata](https://www.youtube.com/watch?v=Sno10WQ21Zs&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #45: [Automatic generate & serve Swagger docs from Go server](https://www.youtube.com/watch?v=Uwkxxee7tvk&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #46: [Embed static frontend files inside Golang backend server's binary](https://www.youtube.com/watch?v=xNgOIm86N5Q&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #47: [Validate gRPC parameters and send human/machine friendly response](https://www.youtube.com/watch?v=CxZ9hMtmZtc&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #48: [Run DB migrations directly inside Golang code](https://www.youtube.com/watch?v=TG43cMpaxlI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #49: [Partial update DB record with SQLC nullable parameters](https://www.youtube.com/watch?v=I2sbw1PzzW0&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #50: [Build gRPC update API with optional parameters](https://www.youtube.com/watch?v=ygqSHIEc8sc&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #51: [Add authorization to protect gRPC API](https://www.youtube.com/watch?v=_jqNs3d99ps&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #52: [Write structured logs for gRPC APIs](https://www.youtube.com/watch?v=tTAxLGrDmPo&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #53: [How to write HTTP logger middleware in Go](https://www.youtube.com/watch?v=Lbiz-PZNiU0&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) ### Section 5: Asynchronous processing with background workers [Asynq + Redis] -- Lecture #54: [Implement background worker in Go with Redis and Asynq](https://www.youtube.com/watch?v=XOXdYs8mKkI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=54) -- Lecture #55: [Integrate async worker to Go web server](https://www.youtube.com/watch?v=eXYKGPEXocM&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=55) -- Lecture #56: [Send async tasks to Redis within a DB transaction](https://www.youtube.com/watch?v=ZfFxdPbgN88&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=56) -- Lecture #57: [How to handle errors and print logs for Go Asynq workers](https://www.youtube.com/watch?v=YgfmPIJRg2U&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=57) -- Lecture #58: [A bit of delay might be good for your async tasks](https://www.youtube.com/watch?v=ILNiZgseLUI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=58) -- Lecture #59: [How to send emails in Go via Gmail](https://www.youtube.com/watch?v=L9TbZxpykLQ&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=59) -- Lecture #60: [How to skip test in Go and config test flag in vscode](https://www.youtube.com/watch?v=0UwZGM9iqTE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=60) -- Lecture #61: [Email verification in Go: design DB and send email](https://www.youtube.com/watch?v=lEHkwDPHrcc&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=61) -- Lecture #62: [Implement email verification API in Go](https://www.youtube.com/watch?v=50ZN-4UNwnY&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=62) -- Lecture #63: [Unit test gRPC API with mock DB & Redis](https://www.youtube.com/watch?v=QFxZlKb7W2k&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=63) -- Lecture #64: [How to test a gRPC API that requires authentication](https://www.youtube.com/watch?v=MI7ucbAlZPM&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=64) +- Lecture #54: [Implement background worker in Go with Redis and Asynq](https://www.youtube.com/watch?v=XOXdYs8mKkI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #55: [Integrate async worker to Go web server](https://www.youtube.com/watch?v=eXYKGPEXocM&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #56: [Send async tasks to Redis within a DB transaction](https://www.youtube.com/watch?v=ZfFxdPbgN88&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #57: [How to handle errors and print logs for Go Asynq workers](https://www.youtube.com/watch?v=YgfmPIJRg2U&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #58: [A bit of delay might be good for your async tasks](https://www.youtube.com/watch?v=ILNiZgseLUI&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #59: [How to send emails in Go via Gmail](https://www.youtube.com/watch?v=L9TbZxpykLQ&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #60: [How to skip test in Go and config test flag in vscode](https://www.youtube.com/watch?v=0UwZGM9iqTE&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #61: [Email verification in Go: design DB and send email](https://www.youtube.com/watch?v=lEHkwDPHrcc&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #62: [Implement email verification API in Go](https://www.youtube.com/watch?v=50ZN-4UNwnY&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #63: [Unit test gRPC API with mock DB & Redis](https://www.youtube.com/watch?v=QFxZlKb7W2k&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #64: [How to test a gRPC API that requires authentication](https://www.youtube.com/watch?v=MI7ucbAlZPM&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) ### Section 6: Improve the stability and security of the server -- Lecture #65: [Config sqlc version 2 for Go and Postgres](https://www.youtube.com/watch?v=FfXE245HZB4&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=65) -- Lecture #66: [Switch DB driver from lib/pq to pgx](https://www.youtube.com/watch?v=m9gYy5U0edQ&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=66) -- Lecture #67: [How to handle DB errors with PGX driver](https://www.youtube.com/watch?v=9vf3zxrMUgw&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=67) -- Lecture #68: [Docker compose: port + volume mapping](https://www.youtube.com/watch?v=nJBT5SKENAw&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=68) -- Lecture #69: [How to install & use binary packages in Go](https://www.youtube.com/watch?v=TnJ4ssoNvkY&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE&index=69) - +- Lecture #65: [Config sqlc version 2 for Go and Postgres](https://www.youtube.com/watch?v=FfXE245HZB4&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #66: [Switch DB driver from lib/pq to pgx](https://www.youtube.com/watch?v=m9gYy5U0edQ&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #67: [How to handle DB errors with PGX driver](https://www.youtube.com/watch?v=9vf3zxrMUgw&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #68: [Docker compose: port + volume mapping](https://www.youtube.com/watch?v=nJBT5SKENAw&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #69: [How to install & use binary packages in Go](https://www.youtube.com/watch?v=TnJ4ssoNvkY&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) +- Lecture #70: [How to install & use binary packages in Go](https://www.youtube.com/watch?v=Py7dRhtuJ3E&list=PLy_6D98if3ULEtXtNSY_2qN21VCKgoQAE) ## Simple bank service diff --git a/api/account_test.go b/api/account_test.go index 036d28a2..68e19c01 100644 --- a/api/account_test.go +++ b/api/account_test.go @@ -35,7 +35,7 @@ func TestGetAccountAPI(t *testing.T) { name: "OK", accountID: account.ID, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -52,7 +52,7 @@ func TestGetAccountAPI(t *testing.T) { name: "UnauthorizedUser", accountID: account.ID, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "unauthorized_user", time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "unauthorized_user", util.DepositorRole, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -82,7 +82,7 @@ func TestGetAccountAPI(t *testing.T) { name: "NotFound", accountID: account.ID, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { @@ -99,7 +99,7 @@ func TestGetAccountAPI(t *testing.T) { name: "InternalError", accountID: account.ID, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -115,7 +115,7 @@ func TestGetAccountAPI(t *testing.T) { name: "InvalidID", accountID: 0, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -169,7 +169,7 @@ func TestCreateAccountAPI(t *testing.T) { "currency": account.Currency, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { arg := db.CreateAccountParams{ @@ -210,7 +210,7 @@ func TestCreateAccountAPI(t *testing.T) { "currency": account.Currency, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -228,7 +228,7 @@ func TestCreateAccountAPI(t *testing.T) { "currency": "invalid", }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -297,7 +297,7 @@ func TestListAccountsAPI(t *testing.T) { pageSize: n, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { arg := db.ListAccountsParams{ @@ -340,7 +340,7 @@ func TestListAccountsAPI(t *testing.T) { pageSize: n, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -359,7 +359,7 @@ func TestListAccountsAPI(t *testing.T) { pageSize: n, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -377,7 +377,7 @@ func TestListAccountsAPI(t *testing.T) { pageSize: 100000, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). diff --git a/api/middleware_test.go b/api/middleware_test.go index ec63dfae..8322f0da 100644 --- a/api/middleware_test.go +++ b/api/middleware_test.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" "github.com/techschool/simplebank/token" + "github.com/techschool/simplebank/util" ) func addAuthorization( @@ -18,9 +19,10 @@ func addAuthorization( tokenMaker token.Maker, authorizationType string, username string, + role string, duration time.Duration, ) { - token, payload, err := tokenMaker.CreateToken(username, duration) + token, payload, err := tokenMaker.CreateToken(username, role, duration) require.NoError(t, err) require.NotEmpty(t, payload) @@ -29,6 +31,9 @@ func addAuthorization( } func TestAuthMiddleware(t *testing.T) { + username := util.RandomOwner() + role := util.DepositorRole + testCases := []struct { name string setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker) @@ -37,7 +42,7 @@ func TestAuthMiddleware(t *testing.T) { { name: "OK", setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "user", time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, username, role, time.Minute) }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusOK, recorder.Code) @@ -54,7 +59,7 @@ func TestAuthMiddleware(t *testing.T) { { name: "UnsupportedAuthorization", setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, "unsupported", "user", time.Minute) + addAuthorization(t, request, tokenMaker, "unsupported", username, role, time.Minute) }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusUnauthorized, recorder.Code) @@ -63,7 +68,7 @@ func TestAuthMiddleware(t *testing.T) { { name: "InvalidAuthorizationFormat", setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, "", "user", time.Minute) + addAuthorization(t, request, tokenMaker, "", username, role, time.Minute) }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusUnauthorized, recorder.Code) @@ -72,7 +77,7 @@ func TestAuthMiddleware(t *testing.T) { { name: "ExpiredToken", setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "user", -time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, username, role, -time.Minute) }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusUnauthorized, recorder.Code) diff --git a/api/token.go b/api/token.go index 924ffd45..7d7b6152 100644 --- a/api/token.go +++ b/api/token.go @@ -68,6 +68,7 @@ func (server *Server) renewAccessToken(ctx *gin.Context) { accessToken, accessPayload, err := server.tokenMaker.CreateToken( refreshPayload.Username, + refreshPayload.Role, server.config.AccessTokenDuration, ) if err != nil { diff --git a/api/transfer_test.go b/api/transfer_test.go index 7c46a575..3f34a689 100644 --- a/api/transfer_test.go +++ b/api/transfer_test.go @@ -49,7 +49,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) @@ -75,7 +75,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user2.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user2.Username, user2.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) @@ -113,7 +113,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(db.Account{}, db.ErrRecordNotFound) @@ -133,7 +133,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) @@ -153,7 +153,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user3.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user3.Username, user3.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account3.ID)).Times(1).Return(account3, nil) @@ -173,7 +173,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) @@ -193,7 +193,7 @@ func TestTransferAPI(t *testing.T) { "currency": "XYZ", }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(0) @@ -212,7 +212,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(0) @@ -231,7 +231,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(1).Return(db.Account{}, sql.ErrConnDone) @@ -250,7 +250,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) diff --git a/api/user.go b/api/user.go index 65ce1a5b..94de4768 100644 --- a/api/user.go +++ b/api/user.go @@ -109,6 +109,7 @@ func (server *Server) loginUser(ctx *gin.Context) { accessToken, accessPayload, err := server.tokenMaker.CreateToken( user.Username, + user.Role, server.config.AccessTokenDuration, ) if err != nil { @@ -118,6 +119,7 @@ func (server *Server) loginUser(ctx *gin.Context) { refreshToken, refreshPayload, err := server.tokenMaker.CreateToken( user.Username, + user.Role, server.config.RefreshTokenDuration, ) if err != nil { diff --git a/db/migration/000005_add_role_to_users.down.sql b/db/migration/000005_add_role_to_users.down.sql new file mode 100644 index 00000000..f40be404 --- /dev/null +++ b/db/migration/000005_add_role_to_users.down.sql @@ -0,0 +1 @@ +ALTER TABLE "users" DROP COLUMN "role"; diff --git a/db/migration/000005_add_role_to_users.up.sql b/db/migration/000005_add_role_to_users.up.sql new file mode 100644 index 00000000..42ac1d6f --- /dev/null +++ b/db/migration/000005_add_role_to_users.up.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "role" varchar NOT NULL DEFAULT 'depositor'; diff --git a/db/sqlc/account.sql.go b/db/sqlc/account.sql.go index 421ea713..9c99de41 100644 --- a/db/sqlc/account.sql.go +++ b/db/sqlc/account.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 // source: account.sql package db diff --git a/db/sqlc/db.go b/db/sqlc/db.go index 4e998b90..bcfcc9d2 100644 --- a/db/sqlc/db.go +++ b/db/sqlc/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 package db diff --git a/db/sqlc/entry.sql.go b/db/sqlc/entry.sql.go index 8027d3a0..09f024cc 100644 --- a/db/sqlc/entry.sql.go +++ b/db/sqlc/entry.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 // source: entry.sql package db diff --git a/db/sqlc/models.go b/db/sqlc/models.go index 990677f0..d238056e 100644 --- a/db/sqlc/models.go +++ b/db/sqlc/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 package db @@ -54,6 +54,7 @@ type User struct { PasswordChangedAt time.Time `json:"password_changed_at"` CreatedAt time.Time `json:"created_at"` IsEmailVerified bool `json:"is_email_verified"` + Role string `json:"role"` } type VerifyEmail struct { diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index 46500e9a..a6e84899 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 package db diff --git a/db/sqlc/session.sql.go b/db/sqlc/session.sql.go index dc2a5302..244b0988 100644 --- a/db/sqlc/session.sql.go +++ b/db/sqlc/session.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 // source: session.sql package db diff --git a/db/sqlc/transfer.sql.go b/db/sqlc/transfer.sql.go index 7df884e7..81bab57a 100644 --- a/db/sqlc/transfer.sql.go +++ b/db/sqlc/transfer.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 // source: transfer.sql package db diff --git a/db/sqlc/user.sql.go b/db/sqlc/user.sql.go index 55cff83b..79b56e9a 100644 --- a/db/sqlc/user.sql.go +++ b/db/sqlc/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 // source: user.sql package db @@ -19,7 +19,7 @@ INSERT INTO users ( email ) VALUES ( $1, $2, $3, $4 -) RETURNING username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified +) RETURNING username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified, role ` type CreateUserParams struct { @@ -45,12 +45,13 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e &i.PasswordChangedAt, &i.CreatedAt, &i.IsEmailVerified, + &i.Role, ) return i, err } const getUser = `-- name: GetUser :one -SELECT username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified FROM users +SELECT username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified, role FROM users WHERE username = $1 LIMIT 1 ` @@ -65,6 +66,7 @@ func (q *Queries) GetUser(ctx context.Context, username string) (User, error) { &i.PasswordChangedAt, &i.CreatedAt, &i.IsEmailVerified, + &i.Role, ) return i, err } @@ -79,7 +81,7 @@ SET is_email_verified = COALESCE($5, is_email_verified) WHERE username = $6 -RETURNING username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified +RETURNING username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified, role ` type UpdateUserParams struct { @@ -109,6 +111,7 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, e &i.PasswordChangedAt, &i.CreatedAt, &i.IsEmailVerified, + &i.Role, ) return i, err } diff --git a/db/sqlc/verify_email.sql.go b/db/sqlc/verify_email.sql.go index fc1e5dd3..5ebfec80 100644 --- a/db/sqlc/verify_email.sql.go +++ b/db/sqlc/verify_email.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.18.0 +// sqlc v1.22.0 // source: verify_email.sql package db diff --git a/doc/db.dbml b/doc/db.dbml index bc396441..c9cc6ecb 100644 --- a/doc/db.dbml +++ b/doc/db.dbml @@ -7,6 +7,7 @@ Project simple_bank { Table users as U { username varchar [pk] + role varchar [not null, default: 'depositor'] hashed_password varchar [not null] full_name varchar [not null] email varchar [unique, not null] diff --git a/doc/schema.sql b/doc/schema.sql index cc1dde50..9157939d 100644 --- a/doc/schema.sql +++ b/doc/schema.sql @@ -1,9 +1,10 @@ -- SQL dump generated using DBML (dbml-lang.org) -- Database: PostgreSQL --- Generated at: 2023-02-25T10:30:37.080Z +-- Generated at: 2023-09-30T12:00:38.491Z CREATE TABLE "users" ( "username" varchar PRIMARY KEY, + "role" varchar NOT NULL DEFAULT 'depositor', "hashed_password" varchar NOT NULL, "full_name" varchar NOT NULL, "email" varchar UNIQUE NOT NULL, diff --git a/gapi/authorization.go b/gapi/authorization.go index e7e5583b..892467db 100644 --- a/gapi/authorization.go +++ b/gapi/authorization.go @@ -14,7 +14,7 @@ const ( authorizationBearer = "bearer" ) -func (server *Server) authorizeUser(ctx context.Context) (*token.Payload, error) { +func (server *Server) authorizeUser(ctx context.Context, accessibleRoles []string) (*token.Payload, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, fmt.Errorf("missing metadata") @@ -42,5 +42,18 @@ func (server *Server) authorizeUser(ctx context.Context) (*token.Payload, error) return nil, fmt.Errorf("invalid access token: %s", err) } + if !hasPermission(payload.Role, accessibleRoles) { + return nil, fmt.Errorf("permission denied") + } + return payload, nil } + +func hasPermission(userRole string, accessibleRoles []string) bool { + for _, role := range accessibleRoles { + if userRole == role { + return true + } + } + return false +} diff --git a/gapi/main_test.go b/gapi/main_test.go index 6a83864a..ab741f60 100644 --- a/gapi/main_test.go +++ b/gapi/main_test.go @@ -26,8 +26,8 @@ func newTestServer(t *testing.T, store db.Store, taskDistributor worker.TaskDist return server } -func newContextWithBearerToken(t *testing.T, tokenMaker token.Maker, username string, duration time.Duration) context.Context { - accessToken, _, err := tokenMaker.CreateToken(username, duration) +func newContextWithBearerToken(t *testing.T, tokenMaker token.Maker, username string, role string, duration time.Duration) context.Context { + accessToken, _, err := tokenMaker.CreateToken(username, role, duration) require.NoError(t, err) bearerToken := fmt.Sprintf("%s %s", authorizationBearer, accessToken) diff --git a/gapi/rpc_create_user_test.go b/gapi/rpc_create_user_test.go index 07b02c98..9bde1400 100644 --- a/gapi/rpc_create_user_test.go +++ b/gapi/rpc_create_user_test.go @@ -53,13 +53,14 @@ func EqCreateUserTxParams(arg db.CreateUserTxParams, password string, user db.Us return eqCreateUserTxParamsMatcher{arg, password, user} } -func randomUser(t *testing.T) (user db.User, password string) { +func randomUser(t *testing.T, role string) (user db.User, password string) { password = util.RandomString(6) hashedPassword, err := util.HashPassword(password) require.NoError(t, err) user = db.User{ Username: util.RandomOwner(), + Role: role, HashedPassword: hashedPassword, FullName: util.RandomOwner(), Email: util.RandomEmail(), @@ -68,7 +69,7 @@ func randomUser(t *testing.T) (user db.User, password string) { } func TestCreateUserAPI(t *testing.T) { - user, password := randomUser(t) + user, password := randomUser(t, util.DepositorRole) testCases := []struct { name string diff --git a/gapi/rpc_login_user.go b/gapi/rpc_login_user.go index 5a1b0457..259ba213 100644 --- a/gapi/rpc_login_user.go +++ b/gapi/rpc_login_user.go @@ -35,6 +35,7 @@ func (server *Server) LoginUser(ctx context.Context, req *pb.LoginUserRequest) ( accessToken, accessPayload, err := server.tokenMaker.CreateToken( user.Username, + user.Role, server.config.AccessTokenDuration, ) if err != nil { @@ -43,6 +44,7 @@ func (server *Server) LoginUser(ctx context.Context, req *pb.LoginUserRequest) ( refreshToken, refreshPayload, err := server.tokenMaker.CreateToken( user.Username, + user.Role, server.config.RefreshTokenDuration, ) if err != nil { diff --git a/gapi/rpc_update_user.go b/gapi/rpc_update_user.go index 2b5ef8d0..2a290cd7 100644 --- a/gapi/rpc_update_user.go +++ b/gapi/rpc_update_user.go @@ -16,7 +16,7 @@ import ( ) func (server *Server) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserResponse, error) { - authPayload, err := server.authorizeUser(ctx) + authPayload, err := server.authorizeUser(ctx, []string{util.BankerRole, util.DepositorRole}) if err != nil { return nil, unauthenticatedError(err) } @@ -26,7 +26,7 @@ func (server *Server) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) return nil, invalidArgumentError(violations) } - if authPayload.Username != req.GetUsername() { + if authPayload.Role != util.BankerRole && authPayload.Username != req.GetUsername() { return nil, status.Errorf(codes.PermissionDenied, "cannot update other user's info") } diff --git a/gapi/rpc_update_user_test.go b/gapi/rpc_update_user_test.go index c73d4c03..c906201e 100644 --- a/gapi/rpc_update_user_test.go +++ b/gapi/rpc_update_user_test.go @@ -18,7 +18,9 @@ import ( ) func TestUpdateUserAPI(t *testing.T) { - user, _ := randomUser(t) + user, _ := randomUser(t, util.DepositorRole) + other, _ := randomUser(t, util.DepositorRole) + banker, _ := randomUser(t, util.BankerRole) newName := util.RandomOwner() newEmail := util.RandomEmail() @@ -65,7 +67,7 @@ func TestUpdateUserAPI(t *testing.T) { Return(updatedUser, nil) }, buildContext: func(t *testing.T, tokenMaker token.Maker) context.Context { - return newContextWithBearerToken(t, tokenMaker, user.Username, time.Minute) + return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute) }, checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) { require.NoError(t, err) @@ -76,6 +78,73 @@ func TestUpdateUserAPI(t *testing.T) { require.Equal(t, newEmail, updatedUser.Email) }, }, + { + name: "BankerCanUpdateUserInfo", + req: &pb.UpdateUserRequest{ + Username: user.Username, + FullName: &newName, + Email: &newEmail, + }, + buildStubs: func(store *mockdb.MockStore) { + arg := db.UpdateUserParams{ + Username: user.Username, + FullName: pgtype.Text{ + String: newName, + Valid: true, + }, + Email: pgtype.Text{ + String: newEmail, + Valid: true, + }, + } + updatedUser := db.User{ + Username: user.Username, + HashedPassword: user.HashedPassword, + FullName: newName, + Email: newEmail, + PasswordChangedAt: user.PasswordChangedAt, + CreatedAt: user.CreatedAt, + IsEmailVerified: user.IsEmailVerified, + } + store.EXPECT(). + UpdateUser(gomock.Any(), gomock.Eq(arg)). + Times(1). + Return(updatedUser, nil) + }, + buildContext: func(t *testing.T, tokenMaker token.Maker) context.Context { + return newContextWithBearerToken(t, tokenMaker, banker.Username, banker.Role, time.Minute) + }, + checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) { + require.NoError(t, err) + require.NotNil(t, res) + updatedUser := res.GetUser() + require.Equal(t, user.Username, updatedUser.Username) + require.Equal(t, newName, updatedUser.FullName) + require.Equal(t, newEmail, updatedUser.Email) + }, + }, + { + name: "OtherDepositorCannotUpdateThisUserInfo", + req: &pb.UpdateUserRequest{ + Username: user.Username, + FullName: &newName, + Email: &newEmail, + }, + buildStubs: func(store *mockdb.MockStore) { + store.EXPECT(). + UpdateUser(gomock.Any(), gomock.Any()). + Times(0) + }, + buildContext: func(t *testing.T, tokenMaker token.Maker) context.Context { + return newContextWithBearerToken(t, tokenMaker, other.Username, other.Role, time.Minute) + }, + checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) { + require.Error(t, err) + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.PermissionDenied, st.Code()) + }, + }, { name: "UserNotFound", req: &pb.UpdateUserRequest{ @@ -90,7 +159,7 @@ func TestUpdateUserAPI(t *testing.T) { Return(db.User{}, db.ErrRecordNotFound) }, buildContext: func(t *testing.T, tokenMaker token.Maker) context.Context { - return newContextWithBearerToken(t, tokenMaker, user.Username, time.Minute) + return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute) }, checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) { require.Error(t, err) @@ -112,7 +181,7 @@ func TestUpdateUserAPI(t *testing.T) { Times(0) }, buildContext: func(t *testing.T, tokenMaker token.Maker) context.Context { - return newContextWithBearerToken(t, tokenMaker, user.Username, time.Minute) + return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute) }, checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) { require.Error(t, err) @@ -134,7 +203,7 @@ func TestUpdateUserAPI(t *testing.T) { Times(0) }, buildContext: func(t *testing.T, tokenMaker token.Maker) context.Context { - return newContextWithBearerToken(t, tokenMaker, user.Username, -time.Minute) + return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, -time.Minute) }, checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) { require.Error(t, err) diff --git a/token/jwt_maker.go b/token/jwt_maker.go index 2df8f2aa..02d7cdd7 100644 --- a/token/jwt_maker.go +++ b/token/jwt_maker.go @@ -24,8 +24,8 @@ func NewJWTMaker(secretKey string) (Maker, error) { } // CreateToken creates a new token for a specific username and duration -func (maker *JWTMaker) CreateToken(username string, duration time.Duration) (string, *Payload, error) { - payload, err := NewPayload(username, duration) +func (maker *JWTMaker) CreateToken(username string, role string, duration time.Duration) (string, *Payload, error) { + payload, err := NewPayload(username, role, duration) if err != nil { return "", payload, err } diff --git a/token/jwt_maker_test.go b/token/jwt_maker_test.go index dab5c205..4fd8b179 100644 --- a/token/jwt_maker_test.go +++ b/token/jwt_maker_test.go @@ -14,12 +14,13 @@ func TestJWTMaker(t *testing.T) { require.NoError(t, err) username := util.RandomOwner() + role := util.DepositorRole duration := time.Minute issuedAt := time.Now() expiredAt := issuedAt.Add(duration) - token, payload, err := maker.CreateToken(username, duration) + token, payload, err := maker.CreateToken(username, role, duration) require.NoError(t, err) require.NotEmpty(t, token) require.NotEmpty(t, payload) @@ -30,6 +31,7 @@ func TestJWTMaker(t *testing.T) { require.NotZero(t, payload.ID) require.Equal(t, username, payload.Username) + require.Equal(t, role, payload.Role) require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second) require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second) } @@ -38,7 +40,7 @@ func TestExpiredJWTToken(t *testing.T) { maker, err := NewJWTMaker(util.RandomString(32)) require.NoError(t, err) - token, payload, err := maker.CreateToken(util.RandomOwner(), -time.Minute) + token, payload, err := maker.CreateToken(util.RandomOwner(), util.DepositorRole, -time.Minute) require.NoError(t, err) require.NotEmpty(t, token) require.NotEmpty(t, payload) @@ -50,7 +52,7 @@ func TestExpiredJWTToken(t *testing.T) { } func TestInvalidJWTTokenAlgNone(t *testing.T) { - payload, err := NewPayload(util.RandomOwner(), time.Minute) + payload, err := NewPayload(util.RandomOwner(), util.DepositorRole, time.Minute) require.NoError(t, err) jwtToken := jwt.NewWithClaims(jwt.SigningMethodNone, payload) diff --git a/token/maker.go b/token/maker.go index 2228d05c..9466e787 100644 --- a/token/maker.go +++ b/token/maker.go @@ -7,7 +7,7 @@ import ( // Maker is an interface for managing tokens type Maker interface { // CreateToken creates a new token for a specific username and duration - CreateToken(username string, duration time.Duration) (string, *Payload, error) + CreateToken(username string, role string, duration time.Duration) (string, *Payload, error) // VerifyToken checks if the token is valid or not VerifyToken(token string) (*Payload, error) diff --git a/token/paseto_maker.go b/token/paseto_maker.go index 0f63d5e2..d855837f 100644 --- a/token/paseto_maker.go +++ b/token/paseto_maker.go @@ -29,8 +29,8 @@ func NewPasetoMaker(symmetricKey string) (Maker, error) { } // CreateToken creates a new token for a specific username and duration -func (maker *PasetoMaker) CreateToken(username string, duration time.Duration) (string, *Payload, error) { - payload, err := NewPayload(username, duration) +func (maker *PasetoMaker) CreateToken(username string, role string, duration time.Duration) (string, *Payload, error) { + payload, err := NewPayload(username, role, duration) if err != nil { return "", payload, err } diff --git a/token/paseto_maker_test.go b/token/paseto_maker_test.go index e7536923..f95573cf 100644 --- a/token/paseto_maker_test.go +++ b/token/paseto_maker_test.go @@ -13,12 +13,13 @@ func TestPasetoMaker(t *testing.T) { require.NoError(t, err) username := util.RandomOwner() + role := util.DepositorRole duration := time.Minute issuedAt := time.Now() expiredAt := issuedAt.Add(duration) - token, payload, err := maker.CreateToken(username, duration) + token, payload, err := maker.CreateToken(username, role, duration) require.NoError(t, err) require.NotEmpty(t, token) require.NotEmpty(t, payload) @@ -29,6 +30,7 @@ func TestPasetoMaker(t *testing.T) { require.NotZero(t, payload.ID) require.Equal(t, username, payload.Username) + require.Equal(t, role, payload.Role) require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second) require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second) } @@ -37,7 +39,7 @@ func TestExpiredPasetoToken(t *testing.T) { maker, err := NewPasetoMaker(util.RandomString(32)) require.NoError(t, err) - token, payload, err := maker.CreateToken(util.RandomOwner(), -time.Minute) + token, payload, err := maker.CreateToken(util.RandomOwner(), util.DepositorRole, -time.Minute) require.NoError(t, err) require.NotEmpty(t, token) require.NotEmpty(t, payload) diff --git a/token/payload.go b/token/payload.go index 9645a554..dccae115 100644 --- a/token/payload.go +++ b/token/payload.go @@ -17,12 +17,13 @@ var ( type Payload struct { ID uuid.UUID `json:"id"` Username string `json:"username"` + Role string `json:"role"` IssuedAt time.Time `json:"issued_at"` ExpiredAt time.Time `json:"expired_at"` } // NewPayload creates a new token payload with a specific username and duration -func NewPayload(username string, duration time.Duration) (*Payload, error) { +func NewPayload(username string, role string, duration time.Duration) (*Payload, error) { tokenID, err := uuid.NewRandom() if err != nil { return nil, err @@ -31,6 +32,7 @@ func NewPayload(username string, duration time.Duration) (*Payload, error) { payload := &Payload{ ID: tokenID, Username: username, + Role: role, IssuedAt: time.Now(), ExpiredAt: time.Now().Add(duration), } diff --git a/util/role.go b/util/role.go new file mode 100644 index 00000000..d311fffd --- /dev/null +++ b/util/role.go @@ -0,0 +1,6 @@ +package util + +const ( + DepositorRole = "depositor" + BankerRole = "banker" +)