Skip to content

Commit

Permalink
Implement login Apis and reformat server code
Browse files Browse the repository at this point in the history
  • Loading branch information
ashiqYousuf committed Nov 8, 2024
1 parent 4b41447 commit c40d624
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 18 deletions.
33 changes: 27 additions & 6 deletions api/server.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,49 @@
package api

import (
"fmt"

db "github.com/ashiqYousuf/sbank/db/sqlc"
"github.com/ashiqYousuf/sbank/token"
"github.com/ashiqYousuf/sbank/util"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)

// Server serves HTTP requests for our banking service
type Server struct {
store db.Store
router *gin.Engine
store db.Store
router *gin.Engine
tokenMaker token.Maker
config util.Config
}

func NewServer(store db.Store) *Server {
server := &Server{store: store}
router := gin.Default()
func NewServer(config util.Config, store db.Store) (*Server, error) {
tokenMaker, err := token.NewPasetoMaker(config.TokenSymmetricKey)
if err != nil {
return nil, fmt.Errorf("cannot create token maker: %w", err)
}

server := &Server{
store: store,
tokenMaker: tokenMaker,
config: config,
}

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("currency", validCurrency)
}

server.setUpRouter()
return server, nil
}

func (server *Server) setUpRouter() {
router := gin.Default()

router.POST("/users", server.createUser)
router.POST("/users/login", server.loginUser)

router.POST("/accounts", server.createAccount)
router.GET("/accounts/:id", server.getAccount)
Expand All @@ -30,7 +52,6 @@ func NewServer(store db.Store) *Server {
router.POST("/transfers", server.createTransfer)

server.router = router
return server
}

// Runs the HTTP server on a specific address
Expand Down
71 changes: 64 additions & 7 deletions api/user.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package api

import (
"database/sql"
"errors"
"net/http"
"time"

Expand All @@ -17,14 +19,25 @@ type createUserRequest struct {
Email string `json:"email" binding:"required,email"`
}

type createUserResponse 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"`
}

// converts db user to userResponse struct (removes sensitive data like passwords)
func newUserResponse(user db.User) userResponse {
return userResponse{
Username: user.Username,
FullName: user.FullName,
Email: user.Email,
PasswordChangedAt: user.PasswordChangedAt,
CreatedAt: user.CreatedAt,
}
}

func (server *Server) createUser(ctx *gin.Context) {
var req createUserRequest

Expand Down Expand Up @@ -59,12 +72,56 @@ func (server *Server) createUser(ctx *gin.Context) {
return
}

rsp := createUserResponse{
Username: user.Username,
FullName: user.FullName,
Email: user.Email,
PasswordChangedAt: user.PasswordChangedAt,
CreatedAt: user.CreatedAt,
rsp := newUserResponse(user)
ctx.JSON(http.StatusOK, rsp)
}

type loginUserRequest struct {
Username string `json:"username" binding:"required,alphanum"`
Password string `json:"password" binding:"required,min=6"`
}

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

func (server *Server) loginUser(ctx *gin.Context) {
var req loginUserRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}

user, err := server.store.GetUser(ctx, req.Username)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
ctx.JSON(http.StatusNotFound, errorResponse(err))
} else {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
}
return
}

err = util.CheckPassword(req.Password, user.HashedPassword)
if err != nil {
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}

// Validation passed, create access token for user
accessToken, err := server.tokenMaker.CreateToken(
user.Username,
server.config.AccessTokenDuration,
)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}

rsp := loginUserResponse{
AccessToken: accessToken,
User: newUserResponse(user),
}

ctx.JSON(http.StatusOK, rsp)
Expand Down
2 changes: 2 additions & 0 deletions app.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
DB_DRIVER=postgres
DB_SOURCE=postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable
SERVER_ADDRESS=0.0.0.0:8080
TOKEN_SYMMETRIC_KEY=12345678901234567890123456789012
ACCESS_TOKEN_DURATION=15m
5 changes: 4 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ func main() {
}

store := db.NewStore(conn)
server := api.NewServer(store)
server, err := api.NewServer(config, store)
if err != nil {
log.Fatal("cannot create server:", err)
}

err = server.Start(config.ServerAddress)
log.Fatal("cannot start server", err)
Expand Down
14 changes: 10 additions & 4 deletions util/config.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package util

import "github.com/spf13/viper"
import (
"time"

"github.com/spf13/viper"
)

// stores all application wide configuration
type Config struct {
DBDriver string `mapstructure:"DB_DRIVER"`
DBSource string `mapstructure:"DB_SOURCE"`
ServerAddress string `mapstructure:"SERVER_ADDRESS"`
DBDriver string `mapstructure:"DB_DRIVER"`
DBSource string `mapstructure:"DB_SOURCE"`
ServerAddress string `mapstructure:"SERVER_ADDRESS"`
TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"`
AccessTokenDuration time.Duration `mapstructure:"ACCESS_TOKEN_DURATION"`
}

// reads configuration from file or env variables
Expand Down

0 comments on commit c40d624

Please sign in to comment.