Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,14 @@ make cluster_stop
Expose a port to database via:
```bash
kubectl port-forward svc/gobank-db-rw 5432:5432
```

Generate db docs via:
```bash
tbls doc
```

Generate swagger docs via:
```bash
swag init -g main.go -o docs
```
56 changes: 56 additions & 0 deletions api/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ type createAccountRequest struct {
Currency string `json:"currency" binding:"required,currency"`
}

// createAccount creates a new account for the authenticated user.
// @Summary Create account
// @Description Create an account in a supported currency for the authenticated user.
// @Tags accounts
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param account body createAccountRequest true "Create account request"
// @Success 200 {object} AccountResponse
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 403 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /accounts [post]
func (server *Server) createAccount(ctx *gin.Context) {
var req createAccountRequest
if err := ctx.ShouldBindWith(&req, binding.JSON); err != nil {
Expand Down Expand Up @@ -50,6 +64,20 @@ type getAccountRequest struct {
ID int64 `uri:"id" binding:"required,min=1"`
}

// getAccount retrieves an account by ID if the authenticated user owns it.
// @Summary Get account
// @Description Get a single account by ID for the authenticated user.
// @Tags accounts
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Account ID"
// @Success 200 {object} AccountResponse
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /accounts/{id} [get]
func (server *Server) getAccount(ctx *gin.Context) {
var req getAccountRequest
if err := ctx.ShouldBindUri(&req); err != nil {
Expand Down Expand Up @@ -81,6 +109,20 @@ type deleteAccountRequest struct {
ID int64 `uri:"id" binding:"required,min=1"`
}

// deleteAccount removes an account by ID if the authenticated user owns it.
// @Summary Delete account
// @Description Delete an account by ID for the authenticated user.
// @Tags accounts
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Account ID"
// @Success 204 {object} nil
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /accounts/{id} [delete]
func (server *Server) deleteAccount(ctx *gin.Context) {
var req deleteAccountRequest
if err := ctx.ShouldBindUri(&req); err != nil {
Expand Down Expand Up @@ -119,6 +161,20 @@ type listAccountsRequest struct {
Size int32 `form:"size" binding:"required,min=5,max=10"`
}

// listAccounts returns paginated accounts owned by the authenticated user.
// @Summary List accounts
// @Description List accounts for the authenticated user with pagination.
// @Tags accounts
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param page query int true "Page number"
// @Param size query int true "Page size"
// @Success 200 {array} AccountResponse
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /accounts [get]
func (server *Server) listAccounts(ctx *gin.Context) {
var req listAccountsRequest
if err := ctx.ShouldBindWith(&req, binding.Query); err != nil {
Expand Down
37 changes: 37 additions & 0 deletions api/docs_models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package api

type ErrorResponse struct {
Error string `json:"error"`
}

type AccountResponse struct {
ID int64 `json:"id"`
Owner string `json:"owner"`
Balance string `json:"balance"`
Currency string `json:"currency"`
CreatedAt string `json:"created_at"`
DeletedAt string `json:"deleted_at,omitempty"`
}

type EntryResponse struct {
ID int64 `json:"id"`
AccountID int64 `json:"account_id"`
Amount string `json:"amount"`
CreatedAt string `json:"created_at"`
}

type TransferResponse struct {
ID int64 `json:"id"`
FromAccountID int64 `json:"from_account_id"`
ToAccountID int64 `json:"to_account_id"`
Amount string `json:"amount"`
CreatedAt string `json:"created_at"`
}

type TransferTxResultResponse struct {
Transfer TransferResponse `json:"transfer"`
FromAccount AccountResponse `json:"from_account"`
ToAccount AccountResponse `json:"to_account"`
FromEntry EntryResponse `json:"from_entry"`
ToEntry EntryResponse `json:"to_entry"`
}
30 changes: 30 additions & 0 deletions api/entries.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ type getEntryRequest struct {
ID int64 `uri:"id" binding:"required,min=1"`
}

// getEntry retrieves a ledger entry by ID for the authenticated user.
// @Summary Get entry
// @Description Get a single entry by ID when it belongs to the authenticated user.
// @Tags entries
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Entry ID"
// @Success 200 {object} EntryResponse
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /entries/{id} [get]
func (server *Server) getEntry(ctx *gin.Context) {
var req getEntryRequest
if err := ctx.ShouldBindUri(&req); err != nil {
Expand Down Expand Up @@ -58,6 +72,22 @@ type listEntriesRequest struct {
Size int32 `form:"size" binding:"required,min=5,max=10"`
}

// listEntries returns paginated entries for an account owned by the authenticated user.
// @Summary List entries
// @Description List entries for a specific account with pagination.
// @Tags entries
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param account_id query int true "Account ID"
// @Param page query int true "Page number"
// @Param size query int true "Page size"
// @Success 200 {array} EntryResponse
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /entries [get]
func (server *Server) listEntries(ctx *gin.Context) {
var req listEntriesRequest

Expand Down
4 changes: 4 additions & 0 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)

type Server struct {
Expand Down Expand Up @@ -50,6 +52,8 @@ func (server *Server) setupRouter() {
router.POST("/users", server.createUser)
router.POST("/users/login", server.loginUser)

router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

authRoutes := router.Group("/").Use(authMiddleware(server.tokenMaker))

authRoutes.POST("/accounts", server.createAccount)
Expand Down
44 changes: 44 additions & 0 deletions api/transfers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ type getTransferRequest struct {
ID int64 `uri:"id" binding:"required,min=1"`
}

// getTransfer retrieves a transfer by ID when the authenticated user participates in it.
// @Summary Get transfer
// @Description Get a transfer by ID when the authenticated user is sender or receiver.
// @Tags transfers
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param id path int true "Transfer ID"
// @Success 200 {object} TransferResponse
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /transfers/{id} [get]
func (server *Server) getTransfer(ctx *gin.Context) {
var req getTransferRequest
if err := ctx.ShouldBindUri(&req); err != nil {
Expand Down Expand Up @@ -71,6 +85,22 @@ type listAccountTransfersRequest struct {
Size int32 `form:"size" binding:"required,min=5,max=10"`
}

// listTransfers returns paginated transfers for an account owned by the authenticated user.
// @Summary List transfers
// @Description List transfers for a specific account with pagination.
// @Tags transfers
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param account_id query int true "Account ID"
// @Param page query int true "Page number"
// @Param size query int true "Page size"
// @Success 200 {array} TransferResponse
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /transfers [get]
func (server *Server) listTransfers(ctx *gin.Context) {
var req listAccountTransfersRequest

Expand Down Expand Up @@ -123,6 +153,20 @@ type transferRequest struct {
Currency string `json:"currency" binding:"required,currency"`
}

// createTransfer executes a transfer between two accounts owned by the authenticated user.
// @Summary Create transfer
// @Description Create a transfer from one account to another in a supported currency.
// @Tags transfers
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param transfer body transferRequest true "Transfer request"
// @Success 200 {object} TransferTxResultResponse
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /transfers [post]
func (server *Server) createTransfer(ctx *gin.Context) {
var req transferRequest
if err := ctx.ShouldBindWith(&req, binding.JSON); err != nil {
Expand Down
39 changes: 32 additions & 7 deletions api/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ type createUserRequest struct {
Email string `json:"email" binding:"required,email"`
}

type userResponse struct {
type UserResponse struct {
Username string `json:"username"`
FullName string `json:"full_name"`
Email string `json:"email"`
PasswordChangedAt time.Time `json:"password_changed_at"`
CreatedAt time.Time `json:"created_at"`
}

func newUserResponse(user db.User) userResponse {
return userResponse{
func newUserResponse(user db.User) UserResponse {
return UserResponse{
Username: user.Username,
FullName: user.FullName,
Email: user.Email,
Expand All @@ -37,6 +37,18 @@ func newUserResponse(user db.User) userResponse {
}
}

// createUser handles user registration.
// @Summary Create user
// @Description Register a new user with username, password, full name, and email.
// @Tags users
// @Accept json
// @Produce json
// @Param user body createUserRequest true "Create user request"
// @Success 200 {object} UserResponse
// @Failure 400 {object} ErrorResponse
// @Failure 403 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /users [post]
func (server *Server) createUser(ctx *gin.Context) {
var req createUserRequest
if err := ctx.ShouldBindWith(&req, binding.JSON); err != nil {
Expand Down Expand Up @@ -80,11 +92,24 @@ type loginUserRequest struct {
Password string `json:"password" binding:"required,min=8"`
}

type loginUserResponse struct {
AccessToken string `json:"access_token"`
User userResponse
type LoginUserResponse struct {
AccessToken string `json:"access_token"`
User UserResponse `json:"user"`
}

// loginUser authenticates a user and returns a JWT access token.
// @Summary Login user
// @Description Authenticate with username and password to receive an access token.
// @Tags users
// @Accept json
// @Produce json
// @Param credentials body loginUserRequest true "Login credentials"
// @Success 200 {object} LoginUserResponse
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /users/login [post]
func (server *Server) loginUser(ctx *gin.Context) {
var req loginUserRequest
if err := ctx.ShouldBindBodyWith(&req, binding.JSON); err != nil {
Expand Down Expand Up @@ -114,7 +139,7 @@ func (server *Server) loginUser(ctx *gin.Context) {
return
}

resp := loginUserResponse{
resp := LoginUserResponse{
AccessToken: accessToken,
User: newUserResponse(user),
}
Expand Down
4 changes: 2 additions & 2 deletions api/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,11 @@ func requireBodyMatchUser(t *testing.T, body *bytes.Buffer, user db.User) {
data, err := io.ReadAll(body)
require.NoError(t, err)

var response userResponse
var response UserResponse
err = json.Unmarshal(data, &response)
require.NoError(t, err)

require.Equal(t, userResponse{
require.Equal(t, UserResponse{
Username: user.Username,
FullName: user.FullName,
Email: user.Email,
Expand Down
Loading